Python from First PrinciplesPost 10 of 20 · 20 of 20 written

10. Type hints and Pydantic

Python · 10 of 203 min readOct 2026

Type hints are ignored by the interpreter but read by tools and libraries, and Pydantic turns them into runtime validation. In LLM systems that combination becomes your contract with a model’s output.

What type hints are

def top_k(scores: dict[str, float], k: int = 5) -> list[tuple[str, float]]:
    return sorted(scores.items(), key=lambda kv: kv[1], reverse=True)[:k]
  • Python does not enforce them. top_k("oops") runs until something breaks inside.
  • Static checkers (mypy, pyright) read them and catch mistakes before you run anything. Editors use them for autocomplete, and AI coding tools write noticeably better code in well-typed codebases.
  • They are stored at runtime in __annotations__, where dataclasses, Pydantic, FastAPI and tool-calling SDKs read them.

The vocabulary

Hint Meaning
int, str, list[int], dict[str, Any] built-in types and generic containers
X | None may be None (older spelling: Optional[X])
A | B either type
Literal["train", "eval"] exactly one of these values
Callable[[int], float] a function taking an int and returning a float
Iterable[T], Sequence[T], Mapping[K, V] the most general thing you need
TypedDict a dict with known keys, such as API message dicts
Protocol a structural interface (Part 7)
def first[T](xs: list[T]) -> T generics (3.12 syntax; older code uses TypeVar)
Annotated[float, Field(gt=0)] a type plus metadata for libraries
Any switch off checking for this value

Guidance: accept broad types (Iterable) and return specific ones (list). Type function signatures first; checkers infer most local variables.

Tensors and types

Static types cannot see tensor shapes or dtypes; to a checker, every tensor is just torch.Tensor. Document shapes in comments, such as # x: (batch, seq, d_model). Libraries like jaxtyping add shape annotations if you want them checked.

Pydantic: validation at runtime

from typing import Literal
from pydantic import BaseModel, Field, ValidationError

class Sentiment(BaseModel):
    label: Literal["positive", "negative", "neutral"]
    confidence: float = Field(ge=0, le=1)
    evidence: list[str] = []

raw = '{"label": "positive", "confidence": 0.92, "evidence": ["loved it"]}'
try:
    result = Sentiment.model_validate_json(raw)
except ValidationError as e:
    print(e.errors())      # exactly which field failed, and why
  • Pydantic parses and, by default, coerces compatible input ("0.9" becomes 0.9). Strict mode refuses coercion.
  • model_dump() and model_dump_json() serialize; model_json_schema() exports a JSON Schema.
  • Pydantic copies mutable defaults per instance, so evidence: list[str] = [] is safe here, unlike in a plain function (Part 2).
  • The companion package pydantic-settings loads typed configuration from environment variables and .env files.

Why Pydantic is central to LLM work

  1. Structured outputs: give the model a JSON Schema (from model_json_schema()), then validate what comes back. Never trust raw model JSON.
  2. Tool calling: tool parameters are JSON Schemas, and many SDKs and frameworks generate them from Pydantic models or typed functions.
  3. API boundaries: FastAPI validates request and response bodies against Pydantic models automatically.
  4. Repair loops: on ValidationError, send e.errors() back to the model and ask for a corrected answer. The messages are precise enough for it to act on.

Dataclass or Pydantic? Use dataclasses for trusted internal data, with no validation cost. Use Pydantic wherever data enters from outside: users, files, APIs and models.

Questions to ask:

  • Where does untrusted data enter this system, and is it validated right there?
  • Would a type checker catch this mistake?
  • What shape and dtype is this tensor, and where is that written down?