Python from First PrinciplesPost 10 of 20 · 20 of 20 written
- How to use this guide
- 01What Python actually is
- 02Names, objects and references
- 03Built-in types and what they cost
- 04Control flow and expressions
- 05Functions in depth
- 06Iteration and generators
- 07Classes and the data model
- 08Context managers and exceptions
- 09Modules, packages, imports and environments
- 10Type hints and Pydantic
- 11The performance model
- 12Concurrency and parallelism
- 13NumPy: the array model everything else copies
- 14Data and serialization
- 15PyTorch mental model
- 16AI systems engineering in Python
- 17Tooling, debugging and testing
- 18Asking the right questions
- 19Roadmap and build-to-learn projects
- 20Glossary and resources
10. Type hints and Pydantic
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"becomes0.9). Strict mode refuses coercion. model_dump()andmodel_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-settingsloads typed configuration from environment variables and.envfiles.
Why Pydantic is central to LLM work
- Structured outputs: give the model a JSON Schema (from
model_json_schema()), then validate what comes back. Never trust raw model JSON. - Tool calling: tool parameters are JSON Schemas, and many SDKs and frameworks generate them from Pydantic models or typed functions.
- API boundaries: FastAPI validates request and response bodies against Pydantic models automatically.
- Repair loops: on
ValidationError, sende.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?