Skip to content
Home Python FastAPI Path Parameters and Query Parameters

FastAPI Path Parameters and Query Parameters

Where developers are forged. · Structured learning · Free forever.
📍 Part of: Python Libraries → Topic 37 of 51
Master FastAPI parameters: handle path and query data with automatic Pydantic validation, optional arguments, and advanced constraints using Query() and Path().
🧑‍💻 Beginner-friendly — no prior Python experience needed
In this tutorial, you'll learn
Master FastAPI parameters: handle path and query data with automatic Pydantic validation, optional arguments, and advanced constraints using Query() and Path().
  • Hierarchical data that identifies a specific resource belongs in path parameters. Non-hierarchical data that filters, sorts, or paginates belongs in query parameters. This distinction is not stylistic — it affects caching, client expectations, and how CDNs handle your URLs.
  • Type hints are the single source of truth. FastAPI reads them to perform validation, type coercion, and OpenAPI documentation generation. Write accurate type hints and the rest follows automatically.
  • Default values determine requirement. An argument without a default is a required parameter — FastAPI will reject requests that omit it. An argument with a default is optional. This is the entire rule.
✦ Plain-English analogy ✦ Real code with output ✦ Interview questions
Quick Answer
  • Path parameters live inside the route URL with curly braces (e.g., /items/{id}) and identify a specific resource
  • Query parameters are any function arguments not found in the path string — used for filtering, sorting, and pagination
  • FastAPI uses Python type hints to automatically validate, convert, and document all parameters via OpenAPI
  • A type mismatch triggers a structured 422 Unprocessable Entity error before your logic executes
  • Annotated is the modern pattern for combining type hints with Query() or Path() validation without breaking IDE autocompletion
  • Default values dictate requirement: no default = required parameter; with default = optional
🚨 START HERE
FastAPI Parameter Debug Cheat Sheet
When a parameter endpoint is failing in production, run these checks in order. Each step narrows the failure surface before you touch any code.
🟡Unexpected 422 responses
Immediate ActionRead the 422 response body before doing anything else — the detail array tells you the exact field, constraint, and failure reason
Commands
curl -s 'http://localhost:8000/forge/search?query=api&page=abc' | python -m json.tool
curl -s 'http://localhost:8000/openapi.json' | python -c "import sys,json; print(json.dumps(json.load(sys.stdin)['paths']['/forge/search'], indent=2))"
Fix NowFix the client to send the correct type matching the OpenAPI schema. Never loosen the server-side validation to accommodate a broken client.
🟡Path parameter not matching nested routes with slashes
Immediate ActionCheck the route definition for the :path converter — its absence is the most common cause of partial path capture
Commands
grep -n 'path' app/routes/*.py
curl -v 'http://localhost:8000/forge/assets/images/logo.png'
Fix NowAdd the :path converter to the route parameter: {file_path:path}. Redeploy and retest with a multi-segment path.
🟡Query parameter ignored or defaulting unexpectedly on every request
Immediate ActionCompare the URL query string key name with the function argument name — case differences are invisible and cause exactly this symptom
Commands
curl -v 'http://localhost:8000/forge/search?query=fastapi&page=2' 2>&1 | grep -i 'page'
python -c "from app.main import app; print([r.path for r in app.routes])"
Fix NowEnsure URL parameter names are exact, case-sensitive matches for function argument names. If the mismatch is in a client SDK, update the SDK and add a contract test to prevent regression.
Production IncidentSilent 422 Storm — Query Parameter Type Mismatch Brings Down Monitoring DashboardA monitoring dashboard's auto-refresh loop sent page=last instead of page=12 after a UI refactor. FastAPI returned 422 on every request, but the dashboard's error handler silently retried with exponential backoff — creating a thundering herd that saturated the API gateway.
SymptomAPI gateway latency spiked from 12ms to 800ms within three minutes of the frontend deploy. The dashboard showed empty data panels. No application-level errors appeared in logs — only a wall of 422 status codes in the access logs, which the on-call engineer initially dismissed as a client misconfiguration.
AssumptionThe team assumed the database was overloaded or that the new deployment had introduced a query regression. They spent two hours profiling slow queries, checking connection pool exhaustion, and rolling back the backend deploy — none of which had any effect, because the backend was functioning correctly the entire time.
Root causeA frontend refactor changed the pagination widget to send the string 'last' instead of a numeric page index when the user was on the final page. The FastAPI endpoint declared page: int — correctly rejecting the invalid input with 422. But the dashboard's HTTP client classified all non-2xx responses as transient network errors and retried aggressively with exponential backoff that had a misconfigured ceiling. The result was thousands of 422 responses per second — not from broken business logic, but from a client that refused to accept a permanent failure signal.
FixAdded client-side input validation in the dashboard to guarantee page values are always integers before the request is issued. Added a specific 422 handler in the HTTP client that raises immediately without retry. Added rate limiting on the API gateway scoped to 4xx response rates per client IP. Added a circuit breaker on the dashboard's HTTP client to halt all retries after three consecutive failures to the same endpoint.
Key Lesson
422 errors are correct, expected behavior — but every HTTP client must treat them as permanent failures, not transient ones worth retryingMonitor 422 rates as a first-class signal, completely separate from 5xx. A spike in 422s points to a client-side contract violation; a spike in 5xx points to a server-side failure. Conflating them wastes hours of the wrong kind of debuggingAlways pair API boundary validation with client-side validation. The API is the last line of defense, not the first — clients should never be sending invalid types to production endpointsExponential backoff without a hard cap and a jitter strategy is a liability. Every backoff implementation should have a max_retries ceiling and a non-retryable error set that includes 4xx responses
Production Debug GuideSymptom → Action mapping for common parameter failures
422 Unprocessable Entity on every request to a specific endpointDo not guess — read the response body first. FastAPI's 422 payload includes a detail array with machine-readable entries. Each entry contains loc (the parameter location and name), msg (a human-readable description of the failure), and type (the Pydantic error code). The type field tells you whether it's a type coercion failure, a constraint violation, or a missing required field. Fix the client to match the schema; do not modify the server to accept invalid input.
Optional query parameter is always None even when provided in the URLParameter name matching is case-sensitive and exact. Verify the URL key matches the function argument name character-for-character. Also check for URL encoding artifacts — spaces should be %20 in query strings, not raw spaces or + characters unless you are using application/x-www-form-urlencoded encoding. If the name matches, check whether a middleware layer is stripping or rewriting query strings before the request reaches FastAPI.
Path parameter captures only part of a URL segment when the value contains slashesStandard path parameters stop matching at the first / character. Use the :path converter in the route decorator: @app.get('/files/{file_path:path}'). Without it, a request to /files/images/branding/logo.png will only bind images to file_path and fail to match the rest. The :path converter is opt-in by design — FastAPI does not apply it automatically.
List query parameter receives a single item instead of a Python listConfirm the type hint is list[str] or List[str], not str. Then confirm the client is repeating the key for each value: ?tag=python&tag=web&tag=api. A single ?tag=python with a list[str] type hint still produces ['python'] — that is correct behavior. If the client is sending a comma-separated string like ?tag=python,web, FastAPI will not split it automatically; you need a custom validator or a preprocessing step.
Swagger UI lists a parameter in the wrong category — path vs queryFastAPI's parameter resolution depends on an exact name match between the curly-brace placeholder in the route string and the function argument name. If you declare @app.get('/users/{user_id}') but the function argument is named id, FastAPI cannot find a matching path placeholder and falls back to treating id as a query parameter. Fix the name to match exactly: user_id in both the route and the function signature.

Parameter handling is the boundary between untrusted client input and your core business logic. In traditional frameworks, validation is an afterthought — you manually cast strings, check for None, and pray edge cases don't slip through. I've seen that pattern collapse under production load more times than I'd like to admit.

FastAPI inverts this model entirely. By leveraging Python's type hinting system, parameters become a typed contract. FastAPI validates against a schema, converts to the correct Python object, and generates interactive OpenAPI documentation — all from your function signature alone. There is no separate validation layer to synchronize, no schema file to keep in sync with your code.

The distinction between path and query parameters is not cosmetic. Path parameters identify a specific resource — they are hierarchical, always required, and semantically load-bearing. Query parameters modify the representation of that resource — they are non-hierarchical, often optional, and composable. Mixing them up produces confusing APIs that frustrate clients, break caching strategies, and signal a shaky understanding of REST resource modeling to anyone reading your routes.

Path Parameters: Resource Identification

Path parameters exist for one reason: to uniquely identify a resource within your URL hierarchy. They are not filters, they are not options, and they are not toggles. If you find yourself putting something optional into a path segment, that is a design smell worth stopping to examine.

FastAPI supports a special syntax called 'Path Converters' that extend the default matching behavior. The most important is the :path converter — it tells FastAPI to match everything remaining in the URL, including forward slashes. Without it, a path parameter stops at the first / it encounters. This makes :path essential for building file explorers, storage proxies, or any service where the resource identifier is itself a hierarchical path.

Path parameters are inherently required at the routing level. If a client omits a path segment, the route simply does not match — the response is a 404, not a 422. The 422 only appears after route matching succeeds and FastAPI attempts to validate the captured value against your type hint and any Path() constraints. This two-stage process means your validation logic never has to handle a missing path parameter; the router handles that before Pydantic is involved.

io/thecodeforge/params/path_params.py · PYTHON
12345678910111213141516171819202122232425262728293031323334353637383940414243444546
from fastapi import FastAPI, Path
from typing import Annotated

app = FastAPI()


@app.get('/forge/users/{user_id}')
def get_user_by_id(
    # Annotated keeps the base type visible to IDEs and type checkers.
    # Path() attaches validation metadata without polluting the default value.
    user_id: Annotated[
        int,
        Path(
            title='User ID',
            description='The internal registry ID of the user. Must be a positive integer.',
            ge=1,
        ),
    ],
):
    """
    Fetch a single user from the internal forge registry by their numeric ID.
    IDs start at 1 — zero and negatives are rejected at the boundary.
    """
    return {'user_id': user_id, 'context': 'internal_forge_registry'}


# The :path converter is required for any parameter that may contain slashes.
# Without it, /forge/assets/images/branding/logo.png would bind only
# 'images' to file_path and the route would 404 on the remainder.
@app.get('/forge/assets/{file_path:path}')
def get_asset_stream(file_path: str):
    """
    Stream a static asset by its full storage path, including subdirectories.
    file_path captures everything after /forge/assets/ including slashes.
    """
    return {'requested_path': file_path}


# GET /forge/users/42
# -> {'user_id': 42, 'context': 'internal_forge_registry'}

# GET /forge/users/0
# -> 422 Unprocessable Entity (ge=1 constraint violated)

# GET /forge/assets/images/branding/logo.png
# -> {'requested_path': 'images/branding/logo.png'}
▶ Output
{'user_id': 42, 'context': 'internal_forge_registry'}
Mental Model
Path Parameters as Resource Coordinates
A path parameter answers exactly one question: which resource? It is a coordinate in your resource hierarchy, not a configuration knob. If you are asking 'how many?' or 'in what order?' — that belongs in a query parameter.
  • Path = identity. Query = representation. Blurring this line produces APIs that are hard to cache, hard to document, and hard to reason about six months later.
  • A missing path segment produces 404 (route not matched), not 422 (validation failed). These are different failure modes and must be handled differently in clients.
  • Use ge=1 or gt=0 on numeric IDs to reject zero and negatives at the boundary — before they reach your database layer and produce confusing errors downstream.
  • The :path converter is strictly opt-in. Standard path parameters stop at the first slash by design; the converter is your explicit declaration that slashes are part of the value.
  • Path parameters arrive as strings at the HTTP layer. FastAPI uses your type hint to coerce them — so int user_id does not mean the URL contains a binary integer; it means FastAPI will attempt to parse the string '42' into the Python integer 42.
📊 Production Insight
Path parameter validation runs after route matching but before your function body executes. That ordering is load-bearing.
If a client sends GET /forge/users/0, FastAPI matches the route, captures '0', converts it to the integer 0, then checks the ge=1 constraint — and returns 422 before your database query fires. This is your first real defense against garbage data reaching persistence layers.
I have seen teams skip the ge=1 constraint because 'the database will just return an empty result anyway.' That is technically true, but it wastes a database round-trip, generates misleading query logs, and silently accepts input that your API contract never intended to support. Validate at the boundary. Always.
🎯 Key Takeaway
Path parameters are resource coordinates — present, validated, and constrained at the routing boundary before your business logic sees them.
Use the :path converter for slash-containing identifiers. Use Annotated + Path() for clean, IDE-friendly validation metadata. Skip constraint validation and you are just deferring the data quality problem downstream.
Choosing Path Parameter Validation
IfResource ID must be a positive integer
UseUse Annotated[int, Path(ge=1)] — rejects zero, negatives, and non-numeric strings before your function body runs
IfParameter may contain slashes (file paths, nested resource identifiers)
UseUse {param:path} converter in the route string — without it, matching stops at the first slash
IfParameter should have a human-readable description in generated docs
UsePass title and description to Path() — these appear directly in Swagger UI and ReDoc without any extra configuration
IfParameter must conform to a specific format like a prefixed ID or UUID
UseUse Path(pattern='^[A-Z]{3}-[0-9]+$') for format enforcement, but benchmark regex cost on hot paths before shipping

Query Parameters: Filtering and Pagination

Query parameters handle everything that modifies how a resource is represented, rather than which resource you are talking about. Filtering a list by status, sorting results by a field, paginating through a large dataset — these all belong in query parameters. They appear after the ? in the URL, are separated by &, and carry no positional significance. /search?q=fastapi&page=2 is semantically identical to /search?page=2&q=fastapi — order does not matter.

In FastAPI, any function argument that does not appear in the route path string is automatically treated as a query parameter. Providing a default value makes it optional. Omitting the default makes it required — FastAPI will reject requests without it and return a 422 with a clear error pointing at the missing field.

FastAPI also supports multi-value query parameters natively. Define a parameter as list[str] and the client sends the same key multiple times: ?tag=python&tag=web&tag=api. FastAPI collects all values and delivers them as a Python list. There is no custom parsing logic required on your end. If the client sends no values for a list parameter, the result is an empty list — unless you set a default with Query().

One thing worth being deliberate about: the boolean type in FastAPI is more permissive than you might expect. It accepts 'true', '1', 'on', 'yes', 'True' as True and their inverses as False. This is intentional — it makes the API compatible with curl, shell scripts, and frontend libraries that serialize booleans differently. Know this behavior exists so it does not surprise you in testing.

io/thecodeforge/params/query_params.py · PYTHON
123456789101112131415161718192021222324252627282930313233343536
from fastapi import FastAPI
from typing import Optional

app = FastAPI()


@app.get('/forge/search')
def list_artifacts(
    query: str,                    # Required — no default means the client must provide this
    page: int = 1,                 # Optional — defaults to page 1 if not provided
    tag: Optional[str] = None,     # Optional — defaults to None, filtering is skipped
    active: bool = True,           # FastAPI accepts 'true', '1', 'yes' as True
):
    """
    Search artifacts in the forge registry.

    - query: full-text search term (required)
    - page: pagination offset, 1-indexed (optional, default 1)
    - tag: filter results to a specific tag (optional)
    - active: include only active artifacts when True (optional, default True)
    """
    return {
        'search_term': query,
        'pagination': {'page': page},
        'filter': {'tag': tag, 'active': active},
    }


# GET /forge/search?query=api
# -> {'search_term': 'api', 'pagination': {'page': 1}, 'filter': {'tag': None, 'active': True}}

# GET /forge/search?query=api&page=3&tag=infra&active=false
# -> {'search_term': 'api', 'pagination': {'page': 3}, 'filter': {'tag': 'infra', 'active': False}}

# GET /forge/search
# -> 422 Unprocessable Entity (query is required, no default provided)
▶ Output
{'search_term': 'api', 'pagination': {'page': 1}, 'filter': {'tag': None, 'active': True}}
⚠ Default Values Create Implicit Contracts You Have to Maintain
📊 Production Insight
Query parameter ordering is irrelevant to FastAPI but it matters enormously for HTTP caching.
CDNs, reverse proxies, and Varnish instances typically cache based on the full URL string. /search?q=fastapi&page=2 and /search?page=2&q=fastapi are different cache keys — they represent the same request but produce two cache entries, halving your effective cache hit rate.
If caching is part of your infrastructure strategy, normalize query parameter order at the API gateway or in your client SDK before requests are issued. A simple alphabetical sort of query keys, applied consistently, can double cache hit rates on read-heavy list endpoints. It is a five-line change with outsized impact.
🎯 Key Takeaway
Query parameters are composable, order-independent modifiers. Default values make them optional — but bare defaults bypass constraint validation. Use Annotated + Query() any time you need a business rule enforced at the boundary, not just a type check.
And if caching matters in your stack, normalize query string order at the gateway. It costs almost nothing and pays off immediately.
Query Parameter Design Decisions
IfParameter is optional with a sensible default
UseUse param: type = default_value — but if constraints matter, wrap in Query(default=value, ge=1) to enforce them on the default path too
IfParameter is optional but has no sensible default
UseUse Optional[type] = None or type | None = None (Python 3.10+) — signals intentional optionality to both FastAPI and readers
IfParameter should accept multiple values from the same key
UseUse list[type] type hint; client repeats the key: ?tag=a&tag=b&tag=c. Do not use comma-separated strings unless you add explicit parsing.
IfParameter needs validation beyond type checking
UseUse Annotated[type, Query(default=..., ge=1, le=100, min_length=1)] — constraints are enforced before your function body runs

Complex Validation with Query()

Type hints handle the common case — an integer is an integer, a string is a string. But real production APIs have business rules that go beyond type correctness. A service identifier must start with a specific prefix. A pagination limit must be between 1 and 100. A search term must be at least three characters long. These are not type constraints; they are domain constraints, and they belong at the API boundary, not buried inside your business logic.

The Query() class is how you express those constraints in FastAPI. It accepts ge (greater than or equal), gt (greater than), le (less than or equal), lt (less than), min_length, max_length, and pattern for regular expression matching. Every constraint is enforced by Pydantic before your function body executes — a request that violates any constraint is rejected with a structured 422 response containing the field name, the failed constraint, and a human-readable message.

The Annotated pattern is the right way to combine Query() with your type hint. Annotated[str, Query(min_length=3, pattern='^FORGE-.*$')] keeps the base type (str) visible to IDEs and type checkers, while attaching the validation metadata separately. The older pattern — param: str = Query(min_length=3) — works but conflates the default value position with the Query object, which confuses IDEs and breaks autocompletion in some editors.

One constraint worth measuring before you ship: regex patterns via Query(pattern=...) run on every request. A poorly anchored or backtracking-prone regex on a high-traffic endpoint will show up in your profiler. Always anchor your patterns (use ^ and $), test them against pathological inputs, and benchmark them under load before deploying on hot paths. For simple guards like minimum length or maximum value, prefer the numeric and length constraints over regex — they are faster and the error messages are clearer.

io/thecodeforge/params/validation.py · PYTHON
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051
from fastapi import FastAPI, Query
from typing import Annotated

app = FastAPI()


@app.get('/forge/audit-logs')
def get_audit_logs(
    # service_id must start with 'FORGE-' and be at least 3 characters.
    # Annotated keeps str visible to IDEs; Query() attaches the constraints.
    service_id: Annotated[
        str,
        Query(
            title='Service Identifier',
            description='Internal service ID. Must use the FORGE- prefix.',
            min_length=3,
            pattern='^FORGE-.*$',
        ),
    ],
    # limit is optional (defaults to 20) but must be between 1 and 100.
    # Query() on a default value enforces the constraint on every request,
    # including those that omit the parameter and trigger the default.
    limit: Annotated[
        int,
        Query(
            title='Result Limit',
            description='Maximum number of log entries to return. Range: 1–100.',
            ge=1,
            le=100,
        ),
    ] = 20,
):
    """
    Retrieve audit log entries for a specific internal service.

    Both parameters are validated before this function body runs.
    Invalid input never reaches the log query logic.
    """
    return {'service': service_id, 'limit': limit}


# GET /forge/audit-logs?service_id=FORGE-AUTH&limit=50
# -> {'service': 'FORGE-AUTH', 'limit': 50}

# GET /forge/audit-logs?service_id=EXTERNAL-SVC&limit=50
# -> 422 Unprocessable Entity
# -> detail: [{'loc': ['query', 'service_id'], 'msg': 'String should match pattern ...', 'type': 'string_pattern_mismatch'}]

# GET /forge/audit-logs?service_id=FORGE-AUTH&limit=500
# -> 422 Unprocessable Entity
# -> detail: [{'loc': ['query', 'limit'], 'msg': 'Input should be less than or equal to 100', 'type': 'less_than_equal'}]
▶ Output
{'service': 'FORGE-AUTH', 'limit': 20}
Mental Model
Validation as an Executable Contract Layer
Query constraints are not documentation hints — they are hard gates enforced by Pydantic on every single request. If a value violates a constraint, it is rejected. There is no fallback, no silent truncation, no coercion to the nearest valid value. This is the behavior you want.
  • Each constraint (ge, le, min_length, pattern) compiles into a Pydantic field validator at app startup — not at request time. The overhead per request is minimal.
  • Validation failures produce structured 422 responses with field-level detail. The loc array tells clients exactly which parameter failed. The type field gives them a machine-readable error code. Build your client error handling around these fields.
  • Constraints run at the application boundary — the earliest possible moment. This keeps invalid data out of your service layer, your database queries, and your audit logs.
  • The pattern parameter uses Python re syntax. Always anchor patterns with ^ and $. An unanchored pattern like FORGE-.* will match 'PREFIX-FORGE-AUTH' and that is probably not what you intend.
  • Combine Annotated with Query() for the cleanest possible signature. The type is the type; the validation metadata is the metadata. They live in different positions for a reason.
📊 Production Insight
Regex validation in Query(pattern=...) is evaluated by Pydantic on every matching request. The cost is typically measured in microseconds for simple patterns, but it is not zero.
I have seen regex catastrophic backtracking surface as latency spikes on endpoints processing 10,000 requests per second — patterns that looked innocent in unit tests became CPU bottlenecks under sustained load. Before deploying a pattern constraint on a hot path, test it with re.compile(pattern).match(worst_case_input) in a loop and measure microseconds per call. If it crosses your latency budget, refactor the pattern or enforce the constraint in a pre-validation step.
General rule: use min_length and max_length for simple guards, reserve pattern for format enforcement like UUID shapes or prefixed identifiers where the structure genuinely matters.
🎯 Key Takeaway
Query() transforms type hints into executable business rules — string patterns, numeric ranges, and length bounds enforced before your function body runs.
Use Annotated for a clean separation between type and validation metadata. Anchor your regex patterns. Measure constraint cost on hot paths before shipping. And read the 422 detail array — it tells you everything you need to fix a broken client.
🗂 Path Parameters vs Query Parameters
When to use each parameter type in FastAPI
AspectPath ParameterQuery Parameter
PurposeIdentify a specific resource — answers 'which one?'Filter, sort, or paginate resources — answers 'in what form?'
URL PositionEmbedded in the route path (e.g., /users/{id})After the ? in the URL (e.g., ?page=2&status=active)
RequirementAlways required — a missing segment produces 404 at routing, before validation runsOptional when a default value is provided; required without one
Validation Error422 if captured value fails type conversion or Path() constraints422 if provided value fails type conversion or Query() constraints
Order SensitivityPositional — the first {placeholder} matches the first path segmentOrder-independent — matched by key name, not position
Multiple ValuesNot supported — path segments are singular by definitionSupported via list[type] type hint; client repeats the key in the URL
OpenAPI PlacementDocumented under 'Parameters' with 'in': 'path'Documented under 'Parameters' with 'in': 'query'
Cache ImpactPart of the URL path — a standard, stable cache key componentPart of the query string — cache behavior varies by proxy; normalize key order to maximize hit rates

🎯 Key Takeaways

  • Hierarchical data that identifies a specific resource belongs in path parameters. Non-hierarchical data that filters, sorts, or paginates belongs in query parameters. This distinction is not stylistic — it affects caching, client expectations, and how CDNs handle your URLs.
  • Type hints are the single source of truth. FastAPI reads them to perform validation, type coercion, and OpenAPI documentation generation. Write accurate type hints and the rest follows automatically.
  • Default values determine requirement. An argument without a default is a required parameter — FastAPI will reject requests that omit it. An argument with a default is optional. This is the entire rule.
  • FastAPI supports multi-value query parameters natively. Define the parameter as list[str] and the client repeats the key: ?tag=python&tag=web. No custom parsing required.
  • Annotated is the correct pattern for combining type hints with Query() or Path() metadata. It keeps the base type visible to IDEs, type checkers, and every tool in the Python ecosystem.
  • The :path converter is required for capturing path parameter values that contain forward slashes. Without it, matching stops at the first slash. This is opt-in by design — you must declare it explicitly.
  • 422 errors contain structured detail arrays — parse them in your clients instead of treating them as opaque failures. The loc, msg, and type fields tell you exactly which parameter failed and why.
  • Regex constraints via Query(pattern=...) have CPU cost on hot paths. Anchor your patterns, test them against adversarial inputs, and measure before deploying on high-traffic endpoints. Prefer numeric and length constraints where they are sufficient.

⚠ Common Mistakes to Avoid

    Using path parameters for filtering or pagination
    Symptom

    URLs become deeply nested and fragile — /items/active/sort/name/page/2 — and every filter combination becomes a unique URL path. Caching breaks because you cannot wildcard filter combinations in most proxy configurations. Adding a new filter requires a new route.

    Fix

    Path parameters belong to resource identity only: /items/{id}. Move all filtering, sorting, and pagination to query parameters: /items?status=active&sort=name&page=2. This is how HTTP was designed and how CDNs expect URLs to behave.

    Assigning Query() or Path() as a default value instead of using Annotated
    Symptom

    IDE autocompletion shows the parameter type as Query or Path object rather than int or str. Type checkers like mypy and pyright report incorrect types. Developers reading the function signature cannot determine the actual type at a glance.

    Fix

    Use Annotated[int, Query(ge=1)] instead of param: int = Query(ge=1). The Annotated pattern keeps the base type in the first position — visible to every tool in the chain — while Query() or Path() carries the validation metadata separately. This is the pattern FastAPI's own documentation recommends and it is the only pattern that works cleanly with all major Python type checkers.

    Not providing a default value for logically optional query parameters
    Symptom

    Clients receive 422 errors when they omit the parameter, even though the API is designed to function without it. This is a contract violation — the API promised the parameter was optional but requires it at runtime.

    Fix

    Assign a default: tag: Optional[str] = None or page: int = 1. A parameter without a default is a required parameter — FastAPI will enforce that. If the parameter is optional in your design, the default value is how you declare that intent.

    Forgetting the :path converter for path parameters that may contain slashes
    Symptom

    A request to /files/documents/quarterly/report.pdf captures only documents as the file_path value. The remaining segments are silently dropped or cause a route mismatch. The bug is invisible in unit tests that use single-segment paths.

    Fix

    Add the :path converter to the route: @app.get('/files/{file_path:path}'). Test explicitly with multi-segment paths in your test suite — use paths with at least two slashes to confirm the converter is working.

    Assuming query parameter names are case-insensitive
    Symptom

    Client sends ?Page=2 but the function argument is page: int. FastAPI does not find a matching argument for Page, treats the request as if page was not provided, and falls back to the default value. The client never receives an error — the page parameter is silently ignored.

    Fix

    Query parameter name matching is case-sensitive and exact. The URL key ?Page= does not match the function argument page. Document the expected casing in your OpenAPI spec and add a contract test that sends the exact key names your API expects. If you receive bug reports about ignored parameters, case sensitivity is the first thing to check.

Interview Questions on This Topic

  • QHow does FastAPI's internal resolution logic determine if a function argument is a Body, Path, or Query parameter?SeniorReveal
    FastAPI resolves parameter categories at application startup — not at request time — which means the schema is built once and reused for every request. The resolution order is: (1) If the argument name appears in curly braces in the route path string, it is a Path parameter. (2) If the argument type is a Pydantic BaseModel subclass, or explicitly annotated with Body(), it is a request body parameter. (3) Simple scalar types — int, str, float, bool — that do not match a path placeholder become Query parameters automatically. (4) Special injection types like Request, Response, BackgroundTasks, and Depends() are handled by FastAPI's dependency system and are not bound to any HTTP parameter category. This resolution is purely structural — it reads your function signature and the route string. There is no runtime introspection per request. The implication is that renaming an argument without updating the route string silently changes a Path parameter into a Query parameter, which is a common source of subtle bugs.
  • QWhy is the Annotated pattern preferred over assigning Query() or Path() directly as a default value in modern FastAPI versions?Mid-levelReveal
    The core issue is separation of concerns between the type system and the validation metadata. When you write param: int = Query(ge=1), the Query object occupies the default value position. Your IDE sees the default as a Query object, not an integer — autocompletion breaks, mypy reports type mismatches, and anyone reading the signature has to mentally parse what Query() returns to understand the actual type. It also makes it awkward to have both a default value and a Query() constraint without workarounds. Annotated[int, Query(ge=1)] solves this cleanly. The first argument to Annotated is always the base type — int — which is what every tool in the chain sees. Everything after the first argument is metadata, invisible to type checkers but readable by FastAPI's parameter resolution. You can have a default value separately: Annotated[int, Query(ge=1)] = 1. This is PEP 593 — Annotated was designed exactly for this use case: attaching runtime metadata to type hints without changing the type. FastAPI's adoption of this pattern aligns it with the broader Python ecosystem standard.
  • QExplain the Path Converter concept. How would you capture a full file path including slashes as a single path parameter?Mid-levelReveal
    By default, FastAPI path parameters use a matching rule that stops at the first forward slash. This is intentional — it makes route matching unambiguous when you have multiple path segments. The :path converter overrides this behavior and tells FastAPI to match everything remaining in the URL after the preceding path segments, including any number of forward slashes. The syntax is {param_name:path} in the route decorator. Example: @app.get('/files/{file_path:path}') with a request to /files/docs/2026/q1/report.pdf will bind docs/2026/q1/report.pdf to file_path. Without the converter, only docs would be captured. Practical applications include file storage APIs, documentation servers, and reverse proxies where the resource identifier is itself a hierarchical path. One important detail: the :path converter captures the raw URL-decoded path string. If the path contains special characters, apply your own sanitization before passing the value to any filesystem operation.
  • QScenario: You have a query parameter tags that should accept a list of strings. How do you define this so it appears correctly in generated OpenAPI documentation?Mid-levelReveal
    Define the parameter as tags: list[str] in the function signature. FastAPI detects the list type hint and generates the correct OpenAPI schema: type: array with items: {type: string}. The parameter appears in Swagger UI with a UI element that allows multiple values. Clients send multiple values by repeating the key: GET /search?tags=python&tags=web&tags=api. FastAPI collects all instances of the tags key and delivers them as the Python list ['python', 'web', 'api']. If the client sends a single value — GET /search?tags=python — FastAPI still produces a list: ['python']. If the client sends no tags key at all, the behavior depends on whether you have provided a default. Without a default, tags is required and the request fails with 422. With tags: list[str] = Query(default=[]), it becomes optional with an empty list as the fallback. One sharp edge: if your client sends a comma-separated string — ?tags=python,web,api — FastAPI does not split it automatically. You would receive ['python,web,api'] as a single-element list. Either document the repeated-key convention or add a custom validator to handle both formats.
  • QIn a high-performance system, how does FastAPI's use of Pydantic for parameter validation impact latency compared to raw dictionary access?SeniorReveal
    Pydantic v2 — the version used by FastAPI since 0.100.0 — rewrote the validation core in Rust. For simple parameter sets (a handful of scalar fields with straightforward type constraints), validation overhead is typically under 1 microsecond per request. At that scale, the performance argument against Pydantic validation essentially disappears. The comparison shifts when you introduce complexity: multiple chained regex patterns, deeply nested model validation, custom validator functions written in Python (not Rust), or validators that perform I/O. Each of those adds measurable latency that raw dict.get() would not incur. The practical trade-off is this: Pydantic gives you automatic type coercion, structured error responses, OpenAPI schema generation, and a documented validation contract — all from your function signature. Raw Request object access gives you a dict.get() and no automatic anything. For endpoints where I need sub-50 microsecond validation budgets, I profile with pydantic's model_validate benchmarks against realistic payloads and decide per endpoint. In four years of FastAPI services, I have had to bypass Pydantic for exactly two endpoints — both were high-frequency internal health check paths where even the schema lookup added noise to latency distributions.

Frequently Asked Questions

What is the difference between a path parameter and a query parameter?

Path parameters are structural parts of the URL that identify a specific resource. /users/123 uses 123 as a path parameter — removing it changes which resource you are addressing, or produces a 404 if the route no longer matches. Query parameters appear after the ? and modify the representation of a resource. /users?role=admin&page=2 returns a filtered, paginated view of the users resource without changing the resource being addressed. The rule of thumb: if the API makes no sense without the value, it is a path parameter. If the API returns a sensible (if broad) result without the value, it is a query parameter.

How does FastAPI handle boolean query parameters?

FastAPI is deliberately permissive with booleans in query parameters to maximize compatibility with clients that serialize booleans differently. The values 'true', '1', 'on', 'yes', and 'True' are all interpreted as Python True. The values 'false', '0', 'off', 'no', and 'False' are all interpreted as Python False. This means curl -d 'active=yes', a JavaScript client sending 'active=true', and a Python script sending 'active=1' all produce the same result. If your API requires stricter boolean semantics — only accepting 'true' or 'false', for example — you will need a custom validator, because FastAPI's built-in boolean coercion is intentionally broad.

Can a query parameter be a list?

Yes, and it works without any extra configuration. Define the parameter as list[str] or List[int] and FastAPI will collect all instances of that key from the query string. A URL like ?id=1&id=2&id=3 produces the Python list [1, 2, 3] for a parameter typed as list[int]. A single ?id=1 produces [1] — still a list, just with one element. If no values are provided and you have a default of Query(default=[]), the function receives an empty list. If you need to accept comma-separated strings like ?ids=1,2,3 instead of repeated keys, that requires a custom validator — FastAPI does not split on commas by default.

What is the 422 Unprocessable Entity error in FastAPI?

A 422 error means the request was structurally valid HTTP — proper headers, parseable URL — but the content was semantically invalid against the API's schema. The most common causes with FastAPI are: sending a string where an integer is expected, omitting a required parameter, violating a constraint like a minimum value or a regex pattern, or sending a value that cannot be coerced to the expected type.

FastAPI automatically generates a detailed JSON response body for every 422. The body contains a detail array where each entry has loc (a list showing the parameter location and name), msg (a human-readable description of what failed), and type (a machine-readable Pydantic error code). Always parse these fields in your client code — they remove the guesswork from debugging parameter failures and make it possible to surface actionable errors to end users.

🔥
Naren Founder & Author

Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.

← PreviousNumPy where, select and piecewise — Conditional Array OperationsNext →FastAPI Request Body and Pydantic Models
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged