Typorithm · easy

Bound path and query parameters at their own locations

pythonfastapiindependent-practice
Saved on this device
Full lesson · 4 learning steps Open any exercise for focused practice
1

Step 1 of 4 · intro card

Bound path and query parameters at their own locations

0%

Concept and contract

Use Annotated, real validation constraints and schema-only metadata deliberately.

Path and query values begin as parts of a request URL. A path identifier and a query limit may both become integers, but they occupy different locations and often need different bounds. Make those locations and policies visible in the endpoint signature.

This route accepts a positive role ID and a limit from one through five. Annotated combines the Python type with Path or Query metadata. The example tests accepted numeric text, an invalid identifier and an excessive limit through the local client.

Mechanism and worked example

FastAPI converts matching URL values and applies the declared constraints before entering the endpoint.

FastAPI converts matching URL values and applies the declared constraints before entering the endpoint. Rejected input produces a validation response. The helper reports error types and locations instead of copying a full error body with possibly sensitive input values into a user-facing message.

The Query declaration also contains json_schema_extra with an example. That is documentation metadata, while ge and le are runtime bounds. Recent FastAPI versions no longer accept arbitrary extra keyword arguments as an informal schema extension mechanism. Use the supported argument and keep documentation distinct from enforcement.

Complete worked example

python

from typing import Annotated
from fastapi import FastAPI, Path, Query
from fastapi.testclient import TestClient

def inspect_example(request):
    app = FastAPI()
    @app.get("/roles/{role_id}")
    def read(role_id: Annotated[int, Path(ge=1)],
             limit: Annotated[int, Query(ge=1, le=5, json_schema_extra={"examples": [2]})] = 2):
        return {"id": role_id, "limit": limit}
    with TestClient(app) as client:
        response = client.get(request["path"], params=request.get("query", {}))
        body = response.json()
        if response.status_code == 422:
            body = [{"at": list(e["loc"]), "type": e["type"]} for e in body["detail"]]
        return {"status": response.status_code, "body": body}
Reasoning
FastAPI converts matching URL values and applies the declared constraints before entering the endpoint. Rejected input produces a validation response. The helper reports error types and locations instead of copying a full error body with possibly sensitive input values into a user-facing message. The Query declaration also contains json_schema_extra with an example. That is documentation metadata, while ge and le are runtime bounds. Recent FastAPI versions no longer accept arbitrary extra keyword arguments as an informal schema extension mechanism. Use the supported argument and keep documentation distinct from enforcement.

Focused decision: use the complete example for context

python

from typing import Annotated
from fastapi import Path, Query

def bounded_role(role_id: Annotated[int, Path(ge=1)],
                 limit: Annotated[int, Query(ge=1, le=5)] = 2):
    return {"id": role_id, "limit": limit}
Focused decision
Reconstruct this focused decision after the warm-up. The complete imports, model or application, and test client remain in the worked example. Typing records practice; only the downloadable computer lab executes Python.

Counterexamples and common mistakes

The counterexample removes the upper runtime bound while retaining a schema example.

The counterexample removes the upper runtime bound while retaining a schema example. An excessive limit then reaches the endpoint. An OpenAPI example is not an allowlist, and a description saying small cannot enforce a numeric maximum. A green documentation page is not evidence that an oversized request is rejected.

Avoid using truthiness to select defaults: a submitted zero should fail a positive bound rather than silently becoming the default. Distinguish missing, malformed and out-of-range input, because clients need different corrections for each. Do not turn every validation error into a successful empty page.

Controlled mistake: observe the actual wrong behavior

python

from typing import Annotated
from fastapi import FastAPI, Path, Query
from fastapi.testclient import TestClient

def inspect_example(request):
    app = FastAPI()
    @app.get("/roles/{role_id}")
    def read(role_id: Annotated[int, Path(ge=1)],
             limit: Annotated[int, Query(ge=1, json_schema_extra={"examples": [2]})] = 2):
        return {"id": role_id, "limit": limit}
    with TestClient(app) as client:
        response = client.get(request["path"], params=request.get("query", {}))
        body = response.json()
        if response.status_code == 422:
            body = [{"at": list(e["loc"]), "type": e["type"]} for e in body["detail"]]
        return {"status": response.status_code, "body": body}
Reasoning
The counterexample removes the upper runtime bound while retaining a schema example. An excessive limit then reaches the endpoint. An OpenAPI example is not an allowlist, and a description saying small cannot enforce a numeric maximum. A green documentation page is not evidence that an oversized request is rejected. Avoid using truthiness to select defaults: a submitted zero should fail a positive bound rather than silently becoming the default. Distinguish missing, malformed and out-of-range input, because clients need different corrections for each. Do not turn every validation error into a successful empty page.

Independent practice: one bounded change

Increase the maximum limit from five to eight.

Increase the maximum limit from five to eight. Retain the lower bound, positive path ID and default of two. This changes one resource policy while preserving the rest of the request contract. Update the schema example only if its illustrative value would otherwise become misleading.

Check eight as accepted, nine as rejected and zero as rejected. Keep a malformed role ID case. Do not slice an unbounded result after fetching it and call that the same policy: this lesson checks the request bound, while later query code must apply an appropriate producer bound too.

Step 1: Accept limits up to eight without weakening positive IDs or the lower limit boundary. Step 2: Retain the working imports and application structure. Change the stated policy, run the supplied practice cases, then explain the newly accepted or rejected boundary.

Practice starter: solve before reading the solution

python

from typing import Annotated
from fastapi import FastAPI, Path, Query
from fastapi.testclient import TestClient

def inspect_example(request):
    app = FastAPI()
    @app.get("/roles/{role_id}")
    def read(role_id: Annotated[int, Path(ge=1)],
             limit: Annotated[int, Query(ge=1, le=5, json_schema_extra={"examples": [2]})] = 2):
        return {"id": role_id, "limit": limit}
    with TestClient(app) as client:
        response = client.get(request["path"], params=request.get("query", {}))
        body = response.json()
        if response.status_code == 422:
            body = [{"at": list(e["loc"]), "type": e["type"]} for e in body["detail"]]
        return {"status": response.status_code, "body": body}

# Independent task: Accept limits up to eight without weakening positive IDs or the lower limit boundary.
Reasoning
Accept limits up to eight without weakening positive IDs or the lower limit boundary.
  • Hint 1: Find the one field or branch that owns this policy.
  • Hint 2: Compare the first changed acceptance case with the retained baseline.

Solution, tests and trade-offs

The solution changes le=5 to le=8.

The solution changes le=5 to le=8. Existing valid requests preserve their normalized integer values. The excessive and nonpositive cases remain validation failures at the query location. A path parsing failure still identifies role_id rather than being attributed to the limit.

An endpoint can also accept repeated query values, enums or a structured query model. Choose the smallest representation that expresses the actual contract. Limits should reflect both a useful client experience and the cost of work behind the endpoint; increasing a bound is a capacity decision in a real service.

Practice solution and acceptance checks

python

from typing import Annotated
from fastapi import FastAPI, Path, Query
from fastapi.testclient import TestClient

def inspect_example(request):
    app = FastAPI()
    @app.get("/roles/{role_id}")
    def read(role_id: Annotated[int, Path(ge=1)],
             limit: Annotated[int, Query(ge=1, le=8, json_schema_extra={"examples": [2]})] = 2):
        return {"id": role_id, "limit": limit}
    with TestClient(app) as client:
        response = client.get(request["path"], params=request.get("query", {}))
        body = response.json()
        if response.status_code == 422:
            body = [{"at": list(e["loc"]), "type": e["type"]} for e in body["detail"]]
        return {"status": response.status_code, "body": body}
Reasoning
The solution changes le=5 to le=8. Existing valid requests preserve their normalized integer values. The excessive and nonpositive cases remain validation failures at the query location. A path parsing failure still identifies role_id rather than being attributed to the limit. An endpoint can also accept repeated query values, enums or a structured query model. Choose the smallest representation that expresses the actual contract. Limits should reflect both a useful client experience and the cost of work behind the endpoint; increasing a bound is a capacity decision in a real service.

Interview reasoning and next step

In an interview, separate Python type information, request-location metadata, runtime constraints and JSON Schema annotations.

In an interview, separate Python type information, request-location metadata, runtime constraints and JSON Schema annotations. Show a test for each actual promise instead of reciting the Annotated spelling. The deliberately excessive limit makes this distinction concrete.

The next lesson moves to form models, where omission and extra fields have changed in recent releases. Do not assume a browser checkbox or a sibling dependency is processed like a simple standalone query integer.

Optional stretch — after the core task works: Add one independently reasoned boundary case and explain its expected value before running it.

Computer practice: https://applydjinn.com/typorithm/python-api-lab-20261105.zip. Use an isolated Python environment and the pinned dependencies. Run verify.py --track fastapi, then edit the working exercise baseline and run --practice fa-parameters. The reference suite rejects meaningful counterexamples and checks values, types and unchanged inputs. All data is owned and all HTTP clients are in-process. No production database, feed, account or paid service is used.

Use Annotated, real validation constraints and schema-only metadata deliberately.