Typorithm · easy

Annotated constraints belong to the value they constrain

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

Step 1 of 4 · intro card

Annotated constraints belong to the value they constrain

0%

Concept and contract

Constrain collection size and individual items without confusing the two.

Annotated attaches validation metadata to a particular type. A limit on a list controls the collection, while a limit inside the item annotation controls each item. These are separate resource and data-quality policies. A short list can still contain an enormous string unless the string itself has a bound.

The request has a page size and optional technology tags. Page size is an integer between one and twenty. Each Tag has a length between two and twelve characters; the list permits at most three tags. Writing both levels makes the accepted shape clear and avoids relying on a downstream database or template to reject oversized values.

Mechanism and worked example

The Tag alias combines str with Field bounds.

The Tag alias combines str with Field bounds. The Filters model then uses list[Tag] and a separate maximum list length. A default_factory provides an empty list when tags are absent, while extra="forbid" makes unexpected top-level request keys visible as errors.

The example returns a normal dumped mapping for accepted input or stable validation error types for rejected input. Follow an empty tag through item validation, then follow four ordinary tags through collection validation. Both are invalid, but they fail different contracts. The supplied cases make that difference observable.

Complete worked example

python

from typing import Annotated
from pydantic import BaseModel, ConfigDict, Field, ValidationError

Tag = Annotated[str, Field(min_length=2, max_length=12)]

class Filters(BaseModel):
    model_config = ConfigDict(extra="forbid")
    page_size: Annotated[int, Field(ge=1, le=20)] = 10
    tags: list[Tag] = Field(default_factory=list, max_length=3)

def inspect_example(payload):
    try:
        return {"value": Filters.model_validate(payload).model_dump()}
    except ValidationError as error:
        return {"error": sorted({e["type"] for e in error.errors()})}
Reasoning
The Tag alias combines str with Field bounds. The Filters model then uses list[Tag] and a separate maximum list length. A default_factory provides an empty list when tags are absent, while extra="forbid" makes unexpected top-level request keys visible as errors. The example returns a normal dumped mapping for accepted input or stable validation error types for rejected input. Follow an empty tag through item validation, then follow four ordinary tags through collection validation. Both are invalid, but they fail different contracts. The supplied cases make that difference observable.

Focused decision: use the complete example for context

python

from typing import Annotated
from pydantic import BaseModel, Field

Tag = Annotated[str, Field(min_length=2, max_length=12)]

class Filters(BaseModel):
    page_size: Annotated[int, Field(ge=1, le=20)] = 10
    tags: list[Tag] = Field(default_factory=list, max_length=3)
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

Putting max_length only on the list does not constrain the contents of each string.

Putting max_length only on the list does not constrain the contents of each string. Conversely, limiting a Tag to twelve characters does not limit how many tags a caller can submit. Neither bound replaces a request-body byte limit at the HTTP layer or an application query budget.

Do not import old examples that attach obsolete arbitrary Field or parameter keyword arguments without checking the current library API. Use supported constraints and json_schema_extra for schema-only metadata. Error locations help distinguish the offending list item from a top-level request error; a friendly UI should preserve that context instead of reporting only invalid request.

Controlled mistake: observe the actual wrong behavior

python

from typing import Annotated
from pydantic import BaseModel, ConfigDict, Field, ValidationError

Tag = str

class Filters(BaseModel):
    model_config = ConfigDict(extra="forbid")
    page_size: Annotated[int, Field(ge=1, le=20)] = 10
    tags: list[Tag] = Field(default_factory=list, max_length=3)

def inspect_example(payload):
    try:
        return {"value": Filters.model_validate(payload).model_dump()}
    except ValidationError as error:
        return {"error": sorted({e["type"] for e in error.errors()})}
Reasoning
Putting max_length only on the list does not constrain the contents of each string. Conversely, limiting a Tag to twelve characters does not limit how many tags a caller can submit. Neither bound replaces a request-body byte limit at the HTTP layer or an application query budget. Do not import old examples that attach obsolete arbitrary Field or parameter keyword arguments without checking the current library API. Use supported constraints and json_schema_extra for schema-only metadata. Error locations help distinguish the offending list item from a top-level request error; a friendly UI should preserve that context instead of reporting only invalid request.

Independent practice: one bounded change

Reduce the maximum length of an individual tag from twelve to six characters.

Reduce the maximum length of an individual tag from twelve to six characters. Retain the maximum number of tags and the page-size policy. The word python should still pass, while fastapi should now fail its item-length bound.

Make the edit at the Tag declaration, then test a list containing more than one item. A fix that truncates every string would change the caller input and conceal the rejected value. A fix that limits the list to six items changes the wrong dimension. The task is to reject an out-of-policy item clearly.

Step 1: Limit each tag to six characters; preserve the collection and page-size bounds. 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 pydantic import BaseModel, ConfigDict, Field, ValidationError

Tag = Annotated[str, Field(min_length=2, max_length=12)]

class Filters(BaseModel):
    model_config = ConfigDict(extra="forbid")
    page_size: Annotated[int, Field(ge=1, le=20)] = 10
    tags: list[Tag] = Field(default_factory=list, max_length=3)

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

# Independent task: Limit each tag to six characters; preserve the collection and page-size bounds.
Reasoning
Limit each tag to six characters; preserve the collection and page-size bounds.
  • Hint 1: The required edit is in the Tag alias.
  • Hint 2: Change max_length=12 to max_length=6, not the list limit.

Solution, tests and trade-offs

The accepted solution changes only the Tag max_length.

The accepted solution changes only the Tag max_length. Validation still returns ordinary strings and still permits an empty tags list. The practice cases include an exactly six-character tag, a seven-character tag and a page-size violation so the unrelated integer contract remains observable.

Bounds reduce known input risks but do not establish the economic cost of a query. A list of two broad filters may still cause an expensive database operation. Pair schema limits with measured query plans, bounded pagination and request budgets when building the service. Keep the model focused on the invariants that it can actually enforce.

Practice solution and acceptance checks

python

from typing import Annotated
from pydantic import BaseModel, ConfigDict, Field, ValidationError

Tag = Annotated[str, Field(min_length=2, max_length=6)]

class Filters(BaseModel):
    model_config = ConfigDict(extra="forbid")
    page_size: Annotated[int, Field(ge=1, le=20)] = 10
    tags: list[Tag] = Field(default_factory=list, max_length=3)

def inspect_example(payload):
    try:
        return {"value": Filters.model_validate(payload).model_dump()}
    except ValidationError as error:
        return {"error": sorted({e["type"] for e in error.errors()})}
Reasoning
The accepted solution changes only the Tag max_length. Validation still returns ordinary strings and still permits an empty tags list. The practice cases include an exactly six-character tag, a seven-character tag and a page-size violation so the unrelated integer contract remains observable. Bounds reduce known input risks but do not establish the economic cost of a query. A list of two broad filters may still cause an expensive database operation. Pair schema limits with measured query plans, bounded pagination and request budgets when building the service. Keep the model focused on the invariants that it can actually enforce.

Interview reasoning and next step

When explaining Annotated, identify the exact type receiving the metadata.

When explaining Annotated, identify the exact type receiving the metadata. Draw list length and item length as separate axes, then classify three small examples. This gives a reviewer a repeatable method instead of a memorized Field declaration.

The next lesson examines strictness. A bound does not by itself say whether numeric strings can become integers. Check normalization, shape, unknown keys and resource limits as distinct choices, and give each boundary an acceptance example that would fail if that specific policy were removed.

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

Constrain collection size and individual items without confusing the two.