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
/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.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__ |