Typorithm · medium

Capstone: a checked, country-scoped report contract

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

Step 1 of 4 · intro card

Capstone: a checked, country-scoped report contract

0%

Concept and contract

Combine nested validation, duplicate rejection and derived counts without inventing market totals.

A report needs more than a collection of individually well-shaped rows. It must describe its scope, reject conflicting identities and explain what each count measures. A count derived from one small observed dataset is not automatically the size of a national or global employment market.

This capstone accepts a selected country and up to three synthetic roles. Every row has a positive identity, a supported country and an explicit active or archived status. Both row and report models reject extras. The models are a data contract for an owned fixture; they do not crawl an employer feed or establish rights to publish external content.

Mechanism and worked example

The report first validates nested Role objects, then checks that each role belongs to the selected country and that row identities are unique within this report.

The report first validates nested Role objects, then checks that each role belongs to the selected country and that row identities are unique within this report. Only an accepted report computes observed_count from its retained rows. No value supplied by a caller is trusted as that derived count.

Trace a report containing one active and one archived role. It has two observed rows. Then compare a duplicated identity and a role from another country: both fail before a successful report is returned. The fixture makes scope and identity rules independently observable rather than assuming a sorted list must already be trustworthy.

Complete worked example

python

from typing import Literal, Self
from pydantic import BaseModel, ConfigDict, Field, ValidationError, computed_field, model_validator

class Role(BaseModel):
    model_config = ConfigDict(extra="forbid")
    id: int = Field(ge=1)
    country: Literal["US", "IL", "UA", "CA"]
    status: Literal["active", "archived"] = "active"

class Report(BaseModel):
    model_config = ConfigDict(extra="forbid")
    country: Literal["US", "IL", "UA", "CA"]
    rows: list[Role] = Field(max_length=3)

    @model_validator(mode="after")
    def scoped(self) -> Self:
        if any(row.country != self.country for row in self.rows):
            raise ValueError("country mismatch")
        if len({row.id for row in self.rows}) != len(self.rows):
            raise ValueError("duplicate identity")
        return self

    @computed_field
    @property
    def observed_count(self) -> int:
        return len(self.rows)

def inspect_example(payload):
    try:
        return {"value": Report.model_validate(payload).model_dump(mode="json")}
    except ValidationError as error:
        return {"error": sorted({e["type"] for e in error.errors()})}
Reasoning
The report first validates nested Role objects, then checks that each role belongs to the selected country and that row identities are unique within this report. Only an accepted report computes observed_count from its retained rows. No value supplied by a caller is trusted as that derived count. Trace a report containing one active and one archived role. It has two observed rows. Then compare a duplicated identity and a role from another country: both fail before a successful report is returned. The fixture makes scope and identity rules independently observable rather than assuming a sorted list must already be trustworthy.

Focused decision: use the complete example for context

python

@computed_field
@property
def observed_count(self) -> int:
    return len(self.rows)
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

Removing the country check admits a well-typed report whose rows disagree with its declared scope.

Removing the country check admits a well-typed report whose rows disagree with its declared scope. That is the controlled counterexample. Similarly, counting rows before deciding whether duplicate identities are allowed can inflate a result. A model cannot infer whether two different IDs refer to the same real external vacancy without an additional identity policy.

Keep the limits of the count visible. observed_count refers to this checked input only, and archived rows are still observed rows. It does not assert complete collection, current availability, deduplication across every source, historical continuity or a representative sample. Those claims require collection and provenance evidence outside the model.

Controlled mistake: observe the actual wrong behavior

python

from typing import Literal, Self
from pydantic import BaseModel, ConfigDict, Field, ValidationError, computed_field, model_validator

class Role(BaseModel):
    model_config = ConfigDict(extra="forbid")
    id: int = Field(ge=1)
    country: Literal["US", "IL", "UA", "CA"]
    status: Literal["active", "archived"] = "active"

class Report(BaseModel):
    model_config = ConfigDict(extra="forbid")
    country: Literal["US", "IL", "UA", "CA"]
    rows: list[Role] = Field(max_length=3)

    @model_validator(mode="after")
    def scoped(self) -> Self:
        if False:
            raise ValueError("country mismatch")
        if len({row.id for row in self.rows}) != len(self.rows):
            raise ValueError("duplicate identity")
        return self

    @computed_field
    @property
    def observed_count(self) -> int:
        return len(self.rows)

def inspect_example(payload):
    try:
        return {"value": Report.model_validate(payload).model_dump(mode="json")}
    except ValidationError as error:
        return {"error": sorted({e["type"] for e in error.errors()})}
Reasoning
Removing the country check admits a well-typed report whose rows disagree with its declared scope. That is the controlled counterexample. Similarly, counting rows before deciding whether duplicate identities are allowed can inflate a result. A model cannot infer whether two different IDs refer to the same real external vacancy without an additional identity policy. Keep the limits of the count visible. observed_count refers to this checked input only, and archived rows are still observed rows. It does not assert complete collection, current availability, deduplication across every source, historical continuity or a representative sample. Those claims require collection and provenance evidence outside the model.

Independent practice: one bounded change

Add an active_count computed field while retaining observed_count and all existing scope checks.

Add an active_count computed field while retaining observed_count and all existing scope checks. Count only rows whose declared status equals active. Do not remove archived rows from the report, change their identities or trust a caller-supplied active_count.

Use the retained complete model and add a small computed property. Test a mixed report, an all-archived report and a country mismatch. Predict both counts for each accepted input. This is a bounded addition to a working contract, not a request to build a crawler, a database or a new reporting framework.

Step 1: Add active_count as a computed property while preserving observed_count, rows, scope and duplicate checks. 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 Literal, Self
from pydantic import BaseModel, ConfigDict, Field, ValidationError, computed_field, model_validator

class Role(BaseModel):
    model_config = ConfigDict(extra="forbid")
    id: int = Field(ge=1)
    country: Literal["US", "IL", "UA", "CA"]
    status: Literal["active", "archived"] = "active"

class Report(BaseModel):
    model_config = ConfigDict(extra="forbid")
    country: Literal["US", "IL", "UA", "CA"]
    rows: list[Role] = Field(max_length=3)

    @model_validator(mode="after")
    def scoped(self) -> Self:
        if any(row.country != self.country for row in self.rows):
            raise ValueError("country mismatch")
        if len({row.id for row in self.rows}) != len(self.rows):
            raise ValueError("duplicate identity")
        return self

    @computed_field
    @property
    def observed_count(self) -> int:
        return len(self.rows)

def inspect_example(payload):
    try:
        return {"value": Report.model_validate(payload).model_dump(mode="json")}
    except ValidationError as error:
        return {"error": sorted({e["type"] for e in error.errors()})}

# Independent task: Add active_count as a computed property while preserving observed_count, rows, scope and duplicate checks.
Reasoning
Add active_count as a computed property while preserving observed_count, rows, scope and duplicate checks.
  • Hint 1: Add another computed property beside observed_count.
  • Hint 2: Sum row.status == "active" over self.rows without filtering the retained rows.

Solution, tests and trade-offs

The solution computes the sum of active-status comparisons over already validated rows.

The solution computes the sum of active-status comparisons over already validated rows. A mixed two-row report therefore has observed_count=2 and active_count=1, while an archived-only report has active_count=0. The country mismatch is still rejected before either successful result is exposed.

Computed values remain a representation of the validated snapshot. If an application later changes a row through an unchecked path, it can break the assumptions behind the report. Keep input validation, trusted updates and publication versioning explicit. A later API course will carry these same contracts through a real request and response path using local clients.

Practice solution and acceptance checks

python

from typing import Literal, Self
from pydantic import BaseModel, ConfigDict, Field, ValidationError, computed_field, model_validator

class Role(BaseModel):
    model_config = ConfigDict(extra="forbid")
    id: int = Field(ge=1)
    country: Literal["US", "IL", "UA", "CA"]
    status: Literal["active", "archived"] = "active"

class Report(BaseModel):
    model_config = ConfigDict(extra="forbid")
    country: Literal["US", "IL", "UA", "CA"]
    rows: list[Role] = Field(max_length=3)

    @model_validator(mode="after")
    def scoped(self) -> Self:
        if any(row.country != self.country for row in self.rows):
            raise ValueError("country mismatch")
        if len({row.id for row in self.rows}) != len(self.rows):
            raise ValueError("duplicate identity")
        return self

    @computed_field
    @property
    def observed_count(self) -> int:
        return len(self.rows)

    @computed_field
    @property
    def active_count(self) -> int:
        return sum(row.status == "active" for row in self.rows)

def inspect_example(payload):
    try:
        return {"value": Report.model_validate(payload).model_dump(mode="json")}
    except ValidationError as error:
        return {"error": sorted({e["type"] for e in error.errors()})}
Reasoning
The solution computes the sum of active-status comparisons over already validated rows. A mixed two-row report therefore has observed_count=2 and active_count=1, while an archived-only report has active_count=0. The country mismatch is still rejected before either successful result is exposed. Computed values remain a representation of the validated snapshot. If an application later changes a row through an unchecked path, it can break the assumptions behind the report. Keep input validation, trusted updates and publication versioning explicit. A later API course will carry these same contracts through a real request and response path using local clients.

Interview reasoning and next step

For a final review, explain the complete boundary in order: input shape, row contracts, scope consistency, duplicate identities, derived output and the limits of the evidence.

For a final review, explain the complete boundary in order: input shape, row contracts, scope consistency, duplicate identities, derived output and the limits of the evidence. Identify the one fixture that would detect each removed rule. This gives a reviewer a practical way to evaluate the design.

Continue with FastAPI only when you can distinguish a model boundary from an HTTP route, authentication dependency and persistence boundary. The service should validate the actual client representation and publish an explicit response contract; the existence of these models does not automatically secure or correctly document every route that might use them.

Optional stretch — after the core task works: Add a fixture with an attempted caller-supplied active_count and explain why extra="forbid" should reject 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-capstone. 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.

Combine nested validation, duplicate rejection and derived counts without inventing market totals.