Optional & Default Params

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.

Code examples

Required, Optional, and Default Parameters

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.

Practical Search Endpoint

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'.

Combining Path and Query Parameters

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.

Key points

Concepts covered

Optional, None, Default Values, Union Types, Required vs Optional