Typorithm · medium

Generic envelopes must retain their item 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

Generic envelopes must retain their item contract

0%

Concept and contract

Validate a Page of roles as Page[Role], not as an untyped collection wrapper.

A generic model captures a relationship between a reusable container and the type of values it holds. A Page of Role objects is useful only if the chosen Role contract is actually retained at validation time. A familiar wrapper name is not a replacement for the item schema.

The Page model declares items as list[T] and an observed total. Role declares an integer ID and a title. The example explicitly validates Page[Role], so each nested item receives the same checks a standalone Role would receive. This is a reusable data shape, not a statement that every API total represents the entire employment market.

Mechanism and worked example

The type variable appears in the items field, and the concrete parameter is supplied at the validation call.

The type variable appears in the items field, and the concrete parameter is supplied at the validation call. A numeric string ID becomes an integer under the Role contract. Missing titles and malformed integer IDs produce nested validation errors instead of silently passing through a dictionary list.

The fixture compares the ordinary dumped page, including normalized item values. Page[Any] is deliberately not equivalent: it keeps arbitrary item values and can retain a string ID. The counterexample uses that weaker parameter to demonstrate why explicitly selecting the generic argument is part of the boundary design.

Complete worked example

python

from typing import Any, Generic, TypeVar
from pydantic import BaseModel, Field, ValidationError

T = TypeVar("T")

class Role(BaseModel):
    id: int
    title: str

class Page(BaseModel, Generic[T]):
    items: list[T]
    total: int = Field(ge=0)

def inspect_example(payload):
    try:
        return {"value": Page[Role].model_validate(payload).model_dump()}
    except ValidationError as error:
        return {"error": sorted({e["type"] for e in error.errors()})}
Reasoning
The type variable appears in the items field, and the concrete parameter is supplied at the validation call. A numeric string ID becomes an integer under the Role contract. Missing titles and malformed integer IDs produce nested validation errors instead of silently passing through a dictionary list. The fixture compares the ordinary dumped page, including normalized item values. Page[Any] is deliberately not equivalent: it keeps arbitrary item values and can retain a string ID. The counterexample uses that weaker parameter to demonstrate why explicitly selecting the generic argument is part of the boundary design.

Focused decision: use the complete example for context

python

from typing import Generic, TypeVar
from pydantic import BaseModel

T = TypeVar("T")

class Page(BaseModel, Generic[T]):
    items: list[T]
    total: int

def parse_roles(payload):
    return Page[Role].model_validate(payload)
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

Do not infer relationships that the envelope never declared.

Do not infer relationships that the envelope never declared. A nonnegative total is not automatically checked against the number of returned items, and even a matching count may refer only to one observed query. A service must define pagination, scope, freshness and completion semantics separately.

Likewise, a generic envelope does not make a private item model appropriate for a public response. Choose a public item type where needed and test emitted fields. Reusing the correct container with the wrong item type can still cause a compatibility or privacy failure even when the result is valid JSON.

Controlled mistake: observe the actual wrong behavior

python

from typing import Any, Generic, TypeVar
from pydantic import BaseModel, Field, ValidationError

T = TypeVar("T")

class Role(BaseModel):
    id: int
    title: str

class Page(BaseModel, Generic[T]):
    items: list[T]
    total: int = Field(ge=0)

def inspect_example(payload):
    try:
        return {"value": Page[Any].model_validate(payload).model_dump()}
    except ValidationError as error:
        return {"error": sorted({e["type"] for e in error.errors()})}
Reasoning
Do not infer relationships that the envelope never declared. A nonnegative total is not automatically checked against the number of returned items, and even a matching count may refer only to one observed query. A service must define pagination, scope, freshness and completion semantics separately. Likewise, a generic envelope does not make a private item model appropriate for a public response. Choose a public item type where needed and test emitted fields. Reusing the correct container with the wrong item type can still cause a compatibility or privacy failure even when the result is valid JSON.

Independent practice: one bounded change

Bound each page to at most two Role items.

Bound each page to at most two Role items. Attach max_length to the generic items field and keep Page[Role] at the validation call. The task changes the collection policy without weakening the item type or changing the meaning of total.

Test two accepted items, three rejected items and a malformed role ID. Do not truncate an oversized submitted page silently: the exercise asks for a rejected input, not a shortened successful result. The retained baseline already shows how the wrapper and item validation fit together.

Step 1: Accept at most two items while retaining the concrete Role item contract. 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 Any, Generic, TypeVar
from pydantic import BaseModel, Field, ValidationError

T = TypeVar("T")

class Role(BaseModel):
    id: int
    title: str

class Page(BaseModel, Generic[T]):
    items: list[T]
    total: int = Field(ge=0)

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

# Independent task: Accept at most two items while retaining the concrete Role item contract.
Reasoning
Accept at most two items while retaining the concrete Role item contract.
  • Hint 1: The items declaration owns the collection bound.
  • Hint 2: Add Field(max_length=2); keep Page[Role] in inspect_example.

Solution, tests and trade-offs

The solution adds Field(max_length=2) to list[T].

The solution adds Field(max_length=2) to list[T]. The concrete role parameter still validates each accepted item and normalizes its ID. The practice fixtures verify that the list bound and a nested item error remain distinguishable rather than replacing both with an empty page.

A bounded payload limits one representation size, not the total work performed by the producer. A database query should apply its own limits before fetching an enormous result. Document whether total is exact, estimated, filtered or merely observed, and avoid presenting a small local fixture as evidence for global market coverage.

Practice solution and acceptance checks

python

from typing import Any, Generic, TypeVar
from pydantic import BaseModel, Field, ValidationError

T = TypeVar("T")

class Role(BaseModel):
    id: int
    title: str

class Page(BaseModel, Generic[T]):
    items: list[T] = Field(max_length=2)
    total: int = Field(ge=0)

def inspect_example(payload):
    try:
        return {"value": Page[Role].model_validate(payload).model_dump()}
    except ValidationError as error:
        return {"error": sorted({e["type"] for e in error.errors()})}
Reasoning
The solution adds Field(max_length=2) to list[T]. The concrete role parameter still validates each accepted item and normalizes its ID. The practice fixtures verify that the list bound and a nested item error remain distinguishable rather than replacing both with an empty page. A bounded payload limits one representation size, not the total work performed by the producer. A database query should apply its own limits before fetching an enormous result. Document whether total is exact, estimated, filtered or merely observed, and avoid presenting a small local fixture as evidence for global market coverage.

Interview reasoning and next step

Explain the role of T by pointing to the field where the concrete type is used, then show the Page[Role] call that establishes it.

Explain the role of T by pointing to the field where the concrete type is used, then show the Page[Role] call that establishes it. Compare one accepted numeric string ID with the untyped counterexample. This demonstrates a preserved type relationship rather than just memorizing generic syntax.

The next lesson loads settings through a separate boundary. Configuration values can also originate as strings, but their source, precedence and secrecy requirements differ from request JSON. Keep those policies explicit instead of reusing an arbitrary model with whatever environment happens to be present.

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

Validate a Page of roles as Page[Role], not as an untyped collection wrapper.