Filtering, pagination, sorting and search options all travel as query parameters.
Any function parameter that isn't in the path and has a simple type (int, str, bool, float, enum...) is read from the query string, the ?key=value&key2=value2 part of the URL. Types work exactly as for path parameters: values arrive as strings and are converted and validated.
Whether a query parameter is required depends only on its default:
limit: int = 10is optional; when it's missing you get 10.q: str | None = Noneis optional; when it's missing you getNone, so test withif q is not None.status: strwith no default is required; leaving it out gives a 422.
Booleans are deliberately forgiving: true, 1, yes and on mean True; false, 0, no and off mean False (case-insensitive). Anything else is a 422.
Path and query parameters mix freely, and the order of function parameters doesn't matter: FastAPI identifies each one by name. A name that appears in the path template is a path parameter; any other simple-typed parameter is a query parameter.
Lists are the one surprise. To accept a repeated key such as ?tag=a&tag=b, write tag: Annotated[list[str], Query()] = []. Without Query(), FastAPI would assume a list parameter is part of a JSON request body. Query() also adds constraints like max_length, which the validation lesson covers.
Keep query parameters for things that select or shape a response (filters, pagination, sort order), not for data you're creating; that belongs in the request body.
Example
from typing import Annotated
from fastapi import FastAPI, Query
from fastapi.testclient import TestClient
app = FastAPI()
@app.get("/users/{user_id}/orders")
def list_orders(
user_id: int, # path parameter
status: str, # required query parameter
skip: int = 0, # optional, defaults to 0
limit: int = 10,
paid: bool | None = None, # optional, None when absent
tag: Annotated[list[str], Query()] = [], # ?tag=a&tag=b
):
return {"user": user_id, "status": status, "page": [skip, limit],
"paid": paid, "tags": tag}
c = TestClient(app)
print(c.get("/users/1/orders?status=open&paid=yes&tag=x&tag=y").json())
# {'user': 1, 'status': 'open', 'page': [0, 10], 'paid': True, 'tags': ['x', 'y']}
print(c.get("/users/1/orders").status_code) # 422: status is requiredRemember
- Simple-typed parameters not in the path come from the query string
- No default = required; a default (often None) = optional
- bool accepts true/false, 1/0, yes/no, on/off
- Repeated keys need Annotated[list[str], Query()]
Watch out for
- Writing q: str = None: it works but lies about the type; use str | None = None.