Query Parameters
Query parameters are the key-value pairs after ? in a URL: /items/?skip=0&limit=10&q=widget. FastAPI treats any function parameter that is not a path parameter and not a Pydantic model as a query parameter.
By the end of this lesson you can: declare optional and required query parameters with type hints, apply constraints with Query, handle multi-value query params, and use Annotated for reusable parameter definitions.
Implicit Query Parameters
Any function parameter without a matching {name} in the path is a query parameter:
from fastapi import APIRouter
router = APIRouter(prefix="/items", tags=["items"])
@router.get("/")
async def list_items(
skip: int = 0,
limit: int = 10,
q: str | None = None,
) -> dict:
return {"skip": skip, "limit": limit, "q": q}
curl "http://localhost:8000/items/"
# → {"skip": 0, "limit": 10, "q": null}
curl "http://localhost:8000/items/?skip=5&limit=20&q=widget"
# → {"skip": 5, "limit": 20, "q": "widget"}
Required Query Parameters
Omit the default to make a query parameter required:
from fastapi import APIRouter
router = APIRouter(prefix="/search", tags=["search"])
@router.get("/")
async def search(
q: str, # Required — no default
page: int = 1,
) -> dict:
return {"query": q, "page": page}
If q is omitted, FastAPI returns 422 Unprocessable Entity with field-level detail.
Advanced Constraints with Query
Use Annotated and Query for validation and OpenAPI metadata:
from typing import Annotated
from fastapi import APIRouter, Query
router = APIRouter(prefix="/items", tags=["items"])
@router.get("/")
async def list_items(
skip: Annotated[int, Query(ge=0, description="Items to skip")] = 0,
limit: Annotated[int, Query(ge=1, le=100)] = 10,
q: Annotated[
str | None,
Query(min_length=2, max_length=50, pattern=r"^[\w\s-]+$"),
] = None,
) -> dict:
return {"skip": skip, "limit": limit, "q": q}
Multi-Value Query Parameters
A single key can appear multiple times: /items/?tag=python&tag=async:
from typing import Annotated
from fastapi import APIRouter, Query
@router.get("/by-tags")
async def filter_by_tags(
tag: Annotated[list[str], Query()] = [],
) -> dict:
return {"tags": tag}
curl "http://localhost:8000/items/by-tags?tag=python&tag=async"
# → {"tags": ["python", "async"]}
Reusable Parameter Definitions
from typing import Annotated
from fastapi import Query
SkipParam = Annotated[int, Query(ge=0, description="Items to skip")]
LimitParam = Annotated[int, Query(ge=1, le=100, description="Max items")]
from app.dependencies.pagination import SkipParam, LimitParam
@router.get("/")
async def list_products(skip: SkipParam = 0, limit: LimitParam = 10) -> dict:
return {"skip": skip, "limit": limit}
Bool Query Parameters
| URL value | Python bool |
|---|---|
true, 1, yes, on | True |
false, 0, no, off | False |
@router.get("/active")
async def list_active(include_archived: bool = False) -> dict:
return {"include_archived": include_archived}
Common Pitfalls
| Pitfall | Cause / Symptom | Fix |
|---|---|---|
| Required query param causing 422 | Client forgot to include it | Add a default or document it in the API contract |
| List param receiving only one value | Missing list[str] type hint | Use list[str] with Query() annotation |
None not accepted but field optional | Using str without | None | Use str | None = None |
Bool param rejecting "true" | Raw string comparison | Let FastAPI coerce — use bool type hint |
| Pattern not shown in docs | pattern= missing in Query(...) | Add pattern argument to Query call |
Hands-On Practice
from typing import Annotated
from enum import Enum
from fastapi import APIRouter, Query
class Genre(str, Enum):
action = "action"
drama = "drama"
comedy = "comedy"
router = APIRouter(prefix="/movies", tags=["movies"])
_movies = [
{"id": 1, "title": "Fast Forward", "genre": "action", "rating": 7.5},
{"id": 2, "title": "The Quiet Storm", "genre": "drama", "rating": 8.2},
{"id": 3, "title": "Blazing Thunder", "genre": "action", "rating": 6.1},
]
@router.get("/")
async def search_movies(
genre: Genre | None = None,
min_rating: Annotated[float, Query(ge=0.0, le=10.0)] = 0.0,
q: Annotated[str | None, Query(min_length=2)] = None,
skip: Annotated[int, Query(ge=0)] = 0,
limit: Annotated[int, Query(ge=1, le=50)] = 10,
) -> list[dict]:
results = _movies
if genre:
results = [m for m in results if m["genre"] == genre.value]
if min_rating > 0:
results = [m for m in results if m["rating"] >= min_rating]
if q:
results = [m for m in results if q.lower() in m["title"].lower()]
return results[skip : skip + limit]
uvicorn app.routers.movies:router --reload --port 8004
curl "http://localhost:8004/movies/?genre=action&min_rating=7.0"
curl "http://localhost:8004/movies/?min_rating=11" -w "\n%{http_code}"
# → 422