Difficulty: Beginner
Understanding how to make parameters optional, required, or provide default values is essential for designing clean APIs. FastAPI uses Python's type system and default values to determine parameter behavior, making this intuitive once you understand the patterns.
A parameter is required when it has no default value. Writing def endpoint(name: str) means the client must provide the name parameter. If they do not, FastAPI returns a 422 error. This is appropriate for parameters that are essential for the endpoint to function.
A parameter is optional when it has a default value. Writing def endpoint(name: str = "World") means the client can omit name, and it will default to 'World'. For truly optional parameters where 'no value' is meaningful, use None as the default: def endpoint(name: str | None = None). The str | None type hint tells FastAPI (and your IDE) that the value can be either a string or None.
The distinction between 'optional with a default' and 'optional with None' matters for your logic. If limit: int = 10, the client can omit it and gets 10. If limit: int | None = None, the client can omit it and your code receives None, which you can check to apply different behavior.
FastAPI supports Union types for parameters that accept multiple types. For example, item_id: int | str accepts both integers and strings. This is less common but useful for APIs that accept multiple identifier formats.
The Annotated type from typing can combine type information with Query metadata while keeping defaults separate. This is the recommended pattern in modern FastAPI. For instance, Annotated[str | None, Query(description="Search term")] = None separates the type annotation, validation rules, and default value clearly.
A common pattern is to have required parameters for the main action and optional parameters for modifiers. For example, a search endpoint might require q (the search term) but make page, limit, and sort optional with sensible defaults. This creates a clean API where simple use cases need minimal parameters, while power users can customize behavior.
from fastapi import FastAPI
app = FastAPI()
@app.get("/example")
def example(
required_param: str, # Required (no default)
with_default: str = "hello", # Optional with string default
nullable: str | None = None, # Optional, can be None
flag: bool = False, # Optional with bool default
):
return {
"required": required_param,
"default": with_default,
"nullable": nullable,
"flag": flag,
}
# GET /example?required_param=test
# -> {"required": "test", "default": "hello", "nullable": null, "flag": false}
# GET /example?required_param=test&nullable=value&flag=true
# -> {"required": "test", "default": "hello", "nullable": "value", "flag": true}
# GET /example
# -> 422 Error: required_param is missing
This shows all four patterns: required (no default), optional with a value default, optional with None default, and optional with a boolean default. Each serves a different purpose in API design.
from typing import Annotated
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/search")
def search(
# Required: the search term
q: Annotated[str, Query(min_length=1, max_length=200)],
# Optional pagination
page: int = 1,
per_page: int = 20,
# Optional filters
category: str | None = None,
min_price: float | None = None,
max_price: float | None = None,
# Optional sorting
sort_by: str = "relevance",
order: str = "desc",
):
filters = {}
if category:
filters["category"] = category
if min_price is not None:
filters["min_price"] = min_price
if max_price is not None:
filters["max_price"] = max_price
return {
"query": q,
"page": page,
"per_page": per_page,
"filters": filters,
"sort_by": sort_by,
"order": order,
}
This real-world search endpoint has one required parameter (q) and many optional ones for pagination, filtering, and sorting. None-defaulted parameters are checked explicitly before use, allowing the code to differentiate between 'not provided' and 'provided with a value'.
from fastapi import FastAPI
app = FastAPI()
@app.get("/users/{user_id}/posts")
def get_user_posts(
user_id: int, # Path parameter (required)
published: bool = True, # Query parameter (optional)
skip: int = 0, # Query parameter (optional)
limit: int = 10, # Query parameter (optional)
tag: str | None = None, # Query parameter (optional)
):
return {
"user_id": user_id,
"published": published,
"skip": skip,
"limit": limit,
"tag": tag,
}
# GET /users/42/posts
# -> all defaults for query params, user_id=42
# GET /users/42/posts?published=false&tag=python
# -> user_id=42, published=False, tag="python"
FastAPI automatically distinguishes path parameters (user_id is in the URL path) from query parameters (everything else). Path parameters are always required, while query parameters can have defaults.
Optional, None, Default Values, Union Types, Required vs Optional