Typorithm · medium

Form omission and sibling fields need an explicit policy

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

Step 1 of 4 · intro card

Form omission and sibling fields need an explicit policy

0%

Concept and contract

Exercise current boolean defaults and extra-field handling rather than relying on an old checkbox assumption.

HTML forms submit a different representation from JSON. An unchecked checkbox is commonly omitted rather than sent as an explicit false value. The model must therefore distinguish a declared default from a required field. FastAPI 0.162 changed omitted boolean form handling to follow the declared default or missing-field rule.

The owned fixture uses a form model with a true default for notifications. An omitted notifications field keeps true, while an explicit false string becomes false. model_dump(exclude_unset=True) shows which fields were actually supplied, making omission visible rather than guessing from the final boolean value.

Mechanism and worked example

The form model forbids extras and declares the nonce field used by a sibling Form parameter.

The form model forbids extras and declares the nonce field used by a sibling Form parameter. Since FastAPI 0.160, the model extra policy considers all fields at that request location, including sibling parameters and dependencies. Declaring a shared form field explicitly prevents an intended sibling value from becoming an accidental extra.

The endpoint returns normalized settings, the supplied-field projection and the sibling nonce. The fixture sends local form data with python-multipart installed from the pinned lock. It never uploads files or connects to a real account. Tests compare omission, an explicit false and an unexpected admin field.

Complete worked example

python

from typing import Annotated
from fastapi import FastAPI, Form
from fastapi.testclient import TestClient
from pydantic import BaseModel, ConfigDict

class Preferences(BaseModel):
    model_config = ConfigDict(extra="forbid")
    notifications: bool = True
    nonce: str = "demo"

def inspect_example(request):
    app = FastAPI()
    @app.post("/preferences")
    def save(preferences: Annotated[Preferences, Form()],
             nonce: Annotated[str, Form()] = "demo"):
        return {"effective": preferences.model_dump(),
                "supplied": preferences.model_dump(exclude_unset=True), "nonce": nonce}
    with TestClient(app) as client:
        response = client.post("/preferences", data=request)
        body = response.json()
        if response.status_code == 422:
            body = sorted({e["type"] for e in body["detail"]})
        return {"status": response.status_code, "body": body}
Reasoning
The form model forbids extras and declares the nonce field used by a sibling Form parameter. Since FastAPI 0.160, the model extra policy considers all fields at that request location, including sibling parameters and dependencies. Declaring a shared form field explicitly prevents an intended sibling value from becoming an accidental extra. The endpoint returns normalized settings, the supplied-field projection and the sibling nonce. The fixture sends local form data with python-multipart installed from the pinned lock. It never uploads files or connects to a real account. Tests compare omission, an explicit false and an unexpected admin field.

Focused decision: use the complete example for context

python

class Preferences(BaseModel):
    model_config = ConfigDict(extra="forbid")
    notifications: bool = True
    nonce: str = "demo"

def inspect_preferences(preferences):
    return {"effective": preferences.model_dump(),
            "supplied": preferences.model_dump(exclude_unset=True)}
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 silently ignores extras.

The counterexample silently ignores extras. An admin field then disappears into a successful response, hiding a client error. Another common regression is forcing omitted booleans to false: that contradicts this model default and loses the difference between absence and an explicit choice.

Required booleans should remain required when the contract needs a decision. Do not add a permissive default merely to silence validation. When combining a strict form model with sibling fields, review the complete location together; otherwise a previously valid form can become rejected after an upgrade.

Controlled mistake: observe the actual wrong behavior

python

from typing import Annotated
from fastapi import FastAPI, Form
from fastapi.testclient import TestClient
from pydantic import BaseModel, ConfigDict

class Preferences(BaseModel):
    model_config = ConfigDict(extra="ignore")
    notifications: bool = True
    nonce: str = "demo"

def inspect_example(request):
    app = FastAPI()
    @app.post("/preferences")
    def save(preferences: Annotated[Preferences, Form()],
             nonce: Annotated[str, Form()] = "demo"):
        return {"effective": preferences.model_dump(),
                "supplied": preferences.model_dump(exclude_unset=True), "nonce": nonce}
    with TestClient(app) as client:
        response = client.post("/preferences", data=request)
        body = response.json()
        if response.status_code == 422:
            body = sorted({e["type"] for e in body["detail"]})
        return {"status": response.status_code, "body": body}
Reasoning
The counterexample silently ignores extras. An admin field then disappears into a successful response, hiding a client error. Another common regression is forcing omitted booleans to false: that contradicts this model default and loses the difference between absence and an explicit choice. Required booleans should remain required when the contract needs a decision. Do not add a permissive default merely to silence validation. When combining a strict form model with sibling fields, review the complete location together; otherwise a previously valid form can become rejected after an upgrade.

Independent practice: one bounded change

Change only the default notifications value from true to false.

Change only the default notifications value from true to false. Keep explicit true accepted and keep unexpected fields rejected. This resembles changing a product opt-in default while retaining the ability to make an explicit choice. The submitted-field projection should still distinguish omission.

Test an omitted field, an explicit true and an extra key. Preserve nonce handling. Do not alter model_dump to make omission look like an explicitly submitted false, and do not weaken extra=forbid simply to get the form through.

Step 1: Make notifications default to false without hiding omission, rejecting true, or ignoring extra fields. 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, Form
from fastapi.testclient import TestClient
from pydantic import BaseModel, ConfigDict

class Preferences(BaseModel):
    model_config = ConfigDict(extra="forbid")
    notifications: bool = True
    nonce: str = "demo"

def inspect_example(request):
    app = FastAPI()
    @app.post("/preferences")
    def save(preferences: Annotated[Preferences, Form()],
             nonce: Annotated[str, Form()] = "demo"):
        return {"effective": preferences.model_dump(),
                "supplied": preferences.model_dump(exclude_unset=True), "nonce": nonce}
    with TestClient(app) as client:
        response = client.post("/preferences", data=request)
        body = response.json()
        if response.status_code == 422:
            body = sorted({e["type"] for e in body["detail"]})
        return {"status": response.status_code, "body": body}

# Independent task: Make notifications default to false without hiding omission, rejecting true, or ignoring extra fields.
Reasoning
Make notifications default to false without hiding omission, rejecting true, or ignoring extra fields.
  • 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 the model default.

The solution changes the model default. An omitted field now produces false in the effective settings and stays absent from the exclude_unset projection. An explicit true remains present in both. The extra-field case continues to fail before endpoint work.

For a real preferences form, also implement appropriate authentication, CSRF protection when cookie credentials are used, and durable storage. A successful parse does not establish consent history or authorization. The small fixture isolates representation and version-sensitive validation so those concerns can be tested separately.

Practice solution and acceptance checks

python

from typing import Annotated
from fastapi import FastAPI, Form
from fastapi.testclient import TestClient
from pydantic import BaseModel, ConfigDict

class Preferences(BaseModel):
    model_config = ConfigDict(extra="forbid")
    notifications: bool = False
    nonce: str = "demo"

def inspect_example(request):
    app = FastAPI()
    @app.post("/preferences")
    def save(preferences: Annotated[Preferences, Form()],
             nonce: Annotated[str, Form()] = "demo"):
        return {"effective": preferences.model_dump(),
                "supplied": preferences.model_dump(exclude_unset=True), "nonce": nonce}
    with TestClient(app) as client:
        response = client.post("/preferences", data=request)
        body = response.json()
        if response.status_code == 422:
            body = sorted({e["type"] for e in body["detail"]})
        return {"status": response.status_code, "body": body}
Reasoning
The solution changes the model default. An omitted field now produces false in the effective settings and stays absent from the exclude_unset projection. An explicit true remains present in both. The extra-field case continues to fail before endpoint work. For a real preferences form, also implement appropriate authentication, CSRF protection when cookie credentials are used, and durable storage. A successful parse does not establish consent history or authorization. The small fixture isolates representation and version-sensitive validation so those concerns can be tested separately.

Interview reasoning and next step

Explain why the omitted case is different from the explicit false case even when their effective values match.

Explain why the omitted case is different from the explicit false case even when their effective values match. Then identify how a sibling field interacts with extra=forbid in current FastAPI. A reliable answer connects the upgrade note to observable requests.

Next, examine response models. Accepting a valid form or JSON object is only half a contract; a service must also control which fields are emitted, particularly when internal objects contain account or storage details.

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

Exercise current boolean defaults and extra-field handling rather than relying on an old checkbox assumption.