Docstrings, Type Hints and Function Best Practices

Well-written functions are self-documenting: their name says what they do, their parameter names say what data they expect, their type hints say what types are valid, and their docstring says what a caller needs to know without reading the implementation. These practices are not just style โ€” in FastAPI, type hints on route handler parameters are parsed at runtime by Pydantic to validate incoming request data and generate OpenAPI documentation automatically. A function without type hints in a FastAPI handler is not just poorly documented โ€” it means FastAPI cannot validate input or generate docs for that endpoint.

Docstrings

# A docstring is a string literal as the first statement of a function
# Accessible via the __doc__ attribute and shown in IDEs and help()

def create_user(name: str, email: str, role: str = "user") -> dict:
    """Create a new user and return the user data dict.

    Args:
        name:  The user's display name. Must be 2โ€“100 characters.
        email: A unique email address. Stored lowercase.
        role:  Permission level. One of 'user', 'editor', 'admin'.
               Defaults to 'user'.

    Returns:
        A dict with keys: id, name, email, role, created_at.

    Raises:
        ValueError: If name or email fails validation.
        DuplicateEmailError: If email is already registered.
    """
    # Implementation here
    return {"id": 1, "name": name, "email": email.lower(), "role": role}

# Access the docstring
print(create_user.__doc__)

# One-line docstring for simple functions
def double(n: int) -> int:
    """Return n multiplied by 2."""
    return n * 2
Note: FastAPI reads your route handler’s docstring and uses it as the description in the auto-generated Swagger UI (/docs) and ReDoc (/redoc) API documentation. If you write a good docstring on your FastAPI route handler, you get API documentation for free โ€” no separate documentation file needed. This is one of FastAPI’s biggest productivity advantages over frameworks that require manual OpenAPI spec writing.
Tip: The most common Python docstring formats are Google style (used above โ€” clean and readable), NumPy style (verbose, good for scientific code), and reStructuredText (used by Sphinx documentation generator). For FastAPI projects, Google style is the most popular choice. Pick one format and use it consistently across your entire codebase.
Warning: Type hints in Python are not enforced at runtime by default โ€” the interpreter ignores them. Only Pydantic (used by FastAPI) and static analysis tools like mypy actually use them for validation or checking. If you write def add(a: int, b: int) -> int: and call it with strings, Python will not raise an error โ€” the strings will be concatenated, not added. FastAPI is the exception: it uses Pydantic to enforce type hints on route handler parameters at request time.

Type Hints โ€” Basic Syntax

from typing import Optional, List, Dict, Union

# Parameter type hints
def greet(name: str, times: int = 1) -> str:
    return (f"Hello, {name}! " * times).strip()

# Return type hint
def get_total(prices: list) -> float:
    return sum(prices)

# Optional โ€” parameter that can be None
def find_user(email: str) -> Optional[dict]:   # returns dict or None
    # ... database lookup ...
    return None   # if not found

# List of a specific type
def get_post_ids(posts: List[dict]) -> List[int]:
    return [p["id"] for p in posts]

# Dict with typed keys and values
def build_index(items: List[str]) -> Dict[str, int]:
    return {item: i for i, item in enumerate(items)}

# Union โ€” accepts multiple types
def format_value(value: Union[int, float, str]) -> str:
    return str(value)

# Python 3.10+ โ€” cleaner union syntax with |
def format_value_new(value: int | float | str) -> str:
    return str(value)

# None return type โ€” function returns nothing
def log_event(message: str) -> None:
    print(f"[LOG] {message}")

How FastAPI Uses Type Hints

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import Optional

app = FastAPI()

class PostCreate(BaseModel):
    title: str
    body:  str
    published: bool = False

# FastAPI reads the type hints to:
# 1. Extract 'post_id' from the URL path (int)
# 2. Validate and parse the request body as PostCreate (Pydantic)
# 3. Generate OpenAPI docs automatically
# 4. Return appropriate 422 errors for invalid input

@app.post("/posts/{post_id}")
async def update_post(post_id: int, post: PostCreate) -> dict:
    """Update an existing post.

    - **post_id**: The unique post identifier (from URL path)
    - **post**: The updated post data (from request body)
    """
    # FastAPI has already validated that post_id is an int
    # and that post.title, post.body etc. are the right types
    return {"id": post_id, "title": post.title}

Function Design Best Practices

# โ”€โ”€ 1. Single responsibility โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# BAD: function does too much
def process_user(data):
    # validates, creates, sends email, logs โ€” all in one function
    ...

# GOOD: each function does one thing
def validate_user_data(data: dict) -> bool: ...
def create_user(data: dict) -> dict: ...
def send_welcome_email(user: dict) -> None: ...
def log_user_created(user_id: int) -> None: ...

# โ”€โ”€ 2. DRY โ€” Don't Repeat Yourself โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# BAD: validation logic repeated in multiple functions
def create_post(title, body):
    if not title or len(title) < 3:
        raise ValueError("Title too short")
    ...

def update_post(title, body):
    if not title or len(title) < 3:    # repeated!
        raise ValueError("Title too short")
    ...

# GOOD: extract shared logic
def validate_title(title: str) -> None:
    if not title or len(title) < 3:
        raise ValueError("Title must be at least 3 characters")

def create_post(title: str, body: str) -> dict:
    validate_title(title)    # reuse โœ“
    ...

def update_post(title: str, body: str) -> dict:
    validate_title(title)    # reuse โœ“
    ...

# โ”€โ”€ 3. Descriptive names โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# BAD: cryptic names
def proc(d, f=True): ...
def calc(a, b): ...

# GOOD: self-documenting names
def process_payment(transaction_data: dict, send_receipt: bool = True) -> dict: ...
def calculate_total_with_tax(subtotal: float, tax_rate: float) -> float: ...

# โ”€โ”€ 4. Avoid side effects when possible โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# BAD: modifies the input
def normalise_user(user: dict) -> None:
    user["email"] = user["email"].lower()    # mutates caller's dict!

# GOOD: return a new value
def normalise_user(user: dict) -> dict:
    return {**user, "email": user["email"].lower()}    # โœ“ pure function

Common Mistakes

Mistake 1 โ€” Assuming type hints are enforced at runtime

โŒ Wrong โ€” expecting Python to reject the wrong type:

def add(a: int, b: int) -> int:
    return a + b

add("hello", " world")   # "hello world" โ€” no error! Hints are not enforced

โœ… Correct โ€” type hints are documentation and tooling hints only (except in Pydantic/FastAPI models). Use explicit validation if enforcement is needed at runtime.

Mistake 2 โ€” Docstring describes HOW, not WHAT

โŒ Wrong โ€” docstring restates the implementation:

def get_user(user_id: int) -> dict:
    """Uses the database connection to execute a SELECT query on the users
    table where id equals user_id and returns the result as a dict."""

โœ… Correct โ€” docstring tells the caller what they need to know:

def get_user(user_id: int) -> dict:
    """Return the user with the given ID.

    Returns None if no user with that ID exists.
    Raises DatabaseError if the connection fails.
    """

Mistake 3 โ€” Function with too many parameters

โŒ Wrong โ€” long parameter list is hard to call correctly:

def create_post(title, body, excerpt, published, featured, cover_url, tags, author_id): ...

โœ… Correct โ€” group related parameters into a dataclass or Pydantic model:

class PostCreate(BaseModel):
    title: str;  body: str;  excerpt: str = ""
    published: bool = False; featured: bool = False
    cover_url: str = ""; tags: list = []; author_id: int

def create_post(data: PostCreate) -> dict: ...   # โœ“ one clean parameter

Quick Reference

Pattern Code
Parameter type hint def f(name: str, age: int) -> str:
Optional parameter def f(x: Optional[str] = None):
List of type def f(items: List[int]) -> List[str]:
Union type def f(x: int | str) -> str:
No return def f() -> None:
One-line docstring """Brief description."""
Multi-line docstring Summary, Args, Returns, Raises sections
Access docstring func.__doc__

🧠 Test Yourself

You define a FastAPI route handler: async def get_post(post_id: int): .... A client sends GET /posts/abc. What happens?