Typorithm · medium

Validate external patches before creating the next model

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

Step 1 of 4 · intro card

Validate external patches before creating the next model

0%

Concept and contract

Treat model_copy updates as trusted operations rather than a new validation boundary.

An object that was valid when created can be made invalid by a later unchecked update. model_copy(update=...) is intended for trusted data and does not automatically validate every update against the field contract. A convenient copy API should not be mistaken for a safe request-patch parser.

The Limits model has a positive bounded count. The example starts with a valid count of two, builds a plain mapping of the proposed next state, and runs that mapping through model_validate. This makes the external patch cross the same type and bound checks as an original request.

Mechanism and worked example

current.model_dump creates the baseline mapping and raw.update adds the proposed field values to that separate mapping.

current.model_dump creates the baseline mapping and raw.update adds the proposed field values to that separate mapping. The original model is retained for comparison. A numeric string can be normalized by the declared integer policy; zero and an excessive count are rejected rather than being copied into an apparently valid Limits instance.

The inspection result includes both the validated next value and the unchanged original value. The lab also checks that the input mapping was not mutated. These observations separate a successful validated update from accidentally modifying either the caller data or the previously accepted state.

Complete worked example

python

from pydantic import BaseModel, ConfigDict, Field, ValidationError

class Limits(BaseModel):
    model_config = ConfigDict(extra="ignore")
    count: int = Field(ge=1, le=10)

def inspect_example(payload):
    current = Limits(count=2)
    raw = current.model_dump()
    raw.update(payload)
    try:
        updated = Limits.model_validate(raw)
        return {"value": updated.model_dump(), "original": current.model_dump()}
    except ValidationError as error:
        return {"error": sorted({e["type"] for e in error.errors()})}
Reasoning
current.model_dump creates the baseline mapping and raw.update adds the proposed field values to that separate mapping. The original model is retained for comparison. A numeric string can be normalized by the declared integer policy; zero and an excessive count are rejected rather than being copied into an apparently valid Limits instance. The inspection result includes both the validated next value and the unchanged original value. The lab also checks that the input mapping was not mutated. These observations separate a successful validated update from accidentally modifying either the caller data or the previously accepted state.

Focused decision: use the complete example for context

python

def checked_patch(current, patch):
    proposed = current.model_dump()
    proposed.update(patch)
    return Limits.model_validate(proposed)
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 substitutes current.model_copy(update=payload).

The counterexample substitutes current.model_copy(update=payload). A zero count can then bypass the normal bound. Passing an existing model instance to model_validate is also not universally equivalent to validating an external mapping: instance revalidation has its own configuration and should not be assumed implicitly.

Immutability options solve a different problem. A frozen model prevents certain direct assignments but does not make every nested mutable object deeply immutable or validate trusted copy updates. Decide whether you need revalidation, assignment protection, a deep copy, or a transaction; these words describe different guarantees.

Controlled mistake: observe the actual wrong behavior

python

from pydantic import BaseModel, ConfigDict, Field, ValidationError

class Limits(BaseModel):
    model_config = ConfigDict(extra="ignore")
    count: int = Field(ge=1, le=10)

def inspect_example(payload):
    current = Limits(count=2)
    raw = current.model_dump()
    raw.update(payload)
    try:
        updated = current.model_copy(update=payload)
        return {"value": updated.model_dump(), "original": current.model_dump()}
    except ValidationError as error:
        return {"error": sorted({e["type"] for e in error.errors()})}
Reasoning
The counterexample substitutes current.model_copy(update=payload). A zero count can then bypass the normal bound. Passing an existing model instance to model_validate is also not universally equivalent to validating an external mapping: instance revalidation has its own configuration and should not be assumed implicitly. Immutability options solve a different problem. A frozen model prevents certain direct assignments but does not make every nested mutable object deeply immutable or validate trusted copy updates. Decide whether you need revalidation, assignment protection, a deep copy, or a transaction; these words describe different guarantees.

Independent practice: one bounded change

Keep the validated-patch path and reject unknown patch keys.

Keep the validated-patch path and reject unknown patch keys. The baseline deliberately ignores extras to expose the difference between a valid count update and a misspelled or unrelated field. Change only the extra-field configuration to forbid so an admin key fails instead of disappearing.

Test a valid maximum count, an out-of-range count and an unknown key. Do not fix the test by manually removing unknown fields before validation; that would keep the silent-ignoring behavior. The task is to make unexpected input visible while preserving the original model and the existing numeric contract.

Step 1: Reject unexpected patch keys while retaining full-state validation and the unchanged original model. 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 pydantic import BaseModel, ConfigDict, Field, ValidationError

class Limits(BaseModel):
    model_config = ConfigDict(extra="ignore")
    count: int = Field(ge=1, le=10)

def inspect_example(payload):
    current = Limits(count=2)
    raw = current.model_dump()
    raw.update(payload)
    try:
        updated = Limits.model_validate(raw)
        return {"value": updated.model_dump(), "original": current.model_dump()}
    except ValidationError as error:
        return {"error": sorted({e["type"] for e in error.errors()})}

# Independent task: Reject unexpected patch keys while retaining full-state validation and the unchanged original model.
Reasoning
Reject unexpected patch keys while retaining full-state validation and the unchanged original model.
  • Hint 1: Keep Limits.model_validate(raw) rather than switching to model_copy.
  • Hint 2: Change extra="ignore" to extra="forbid".

Solution, tests and trade-offs

The solution changes the model configuration from ignore to forbid.

The solution changes the model configuration from ignore to forbid. It still merges into a separate ordinary mapping and validates the entire proposed state. The new unknown-key case fails, and the valid count update continues to show an original count of two.

A service patch can also require an allowlist narrower than every field of a storage model. Use a dedicated request model when some attributes are not writable. Field validation does not check object ownership, expected version or concurrent updates. Apply the accepted patch through a transaction that implements those service-level policies when they are needed.

Practice solution and acceptance checks

python

from pydantic import BaseModel, ConfigDict, Field, ValidationError

class Limits(BaseModel):
    model_config = ConfigDict(extra="forbid")
    count: int = Field(ge=1, le=10)

def inspect_example(payload):
    current = Limits(count=2)
    raw = current.model_dump()
    raw.update(payload)
    try:
        updated = Limits.model_validate(raw)
        return {"value": updated.model_dump(), "original": current.model_dump()}
    except ValidationError as error:
        return {"error": sorted({e["type"] for e in error.errors()})}
Reasoning
The solution changes the model configuration from ignore to forbid. It still merges into a separate ordinary mapping and validates the entire proposed state. The new unknown-key case fails, and the valid count update continues to show an original count of two. A service patch can also require an allowlist narrower than every field of a storage model. Use a dedicated request model when some attributes are not writable. Field validation does not check object ownership, expected version or concurrent updates. Apply the accepted patch through a transaction that implements those service-level policies when they are needed.

Interview reasoning and next step

In an interview, distinguish copying, mutation and validation using the zero-count counterexample.

In an interview, distinguish copying, mutation and validation using the zero-count counterexample. Explain why a method name containing model does not guarantee that an external value was checked. Then identify which test would catch a trusted-copy operation accidentally introduced at a request boundary.

The next lesson uses generic models to preserve the relationship between a collection wrapper and its item type. Keep that type relationship explicit at validation time; a generic envelope used without the intended parameter may be a weaker boundary than its name suggests.

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 pydantic, then edit the working exercise baseline and run --practice pyd-copy. 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.

Treat model_copy updates as trusted operations rather than a new validation boundary.