Mid · 7 chapters
Python for AI work
Type hints, pydantic, dependency management, notebooks
Python is the control surface for most AI tooling, and weak project hygiene or debugging skills quickly turn reference code into unreproducible production behavior.
What you will know
Set up a project with a modern dependency manager and a pinned lockfile, from scratch, without copying an old one
Verify a clean checkout can resolve and install the same versions, including how platform-specific markers affect the lockfile.
Write a function whose signature is fully typed, and know when `dict[str, Any]` is hiding a design problem
Static checking only helps when shapes are explicit; unconstrained mappings make invalid states visible only at runtime and at distant call sites.
Define a `pydantic` model, parse untrusted JSON into it, and handle the validation error
Treat rejected input as a normal boundary condition, preserve useful field-level diagnostics, and avoid leaking sensitive payloads in logs.
Replace a notebook-only experiment with a repeatable script when its result needs to run twice
Hidden execution order, mutable state, and unrecorded environment assumptions make a successful notebook particularly easy to misinterpret.
Read a traceback from inside a library and find the line in your own code that caused it
Exception chaining and framework wrappers often bury the actionable frame, so inspect the call boundary and reproduce it with the smallest input.
Chapters
- 1
Python dependency lockfiles
A requirements.txt pinned with == constrains only the packages somebody typed; the thirty-odd transitive packages underneath them stay free to move and nothing is hashed, which is why a clean checkout can resolve differently. This chapter defines resolution and locking, builds a project and its lockfile from scratch with a modern manager, verifies that a fresh clone installs identical versions, and explains why a lock resolved on macOS still hands Linux CI a different set of wheels.
- 2
Type annotations and static checkers
A Python annotation is recorded on the function object and then ignored: the interpreter never consults it, so a signature enforces nothing until a separate program reads it. This chapter shows what the interpreter does with an annotation, annotates a real signature end to end including optionals, unions, container element types and generics, and runs mypy and pyright over the result — default settings against strict ones, and the wiring that makes the check a gate rather than a suggestion.
- 3
The Any type and unconstrained mappings
Any is not a place in the type hierarchy, it is an instruction to the checker to stop checking, and it spreads: anything indexed out of a dict[str, Any] is Any too, so one loose signature can leave a whole call graph unverified. This chapter traces where Any enters a program, why an unconstrained mapping also hides which keys exist, how to count the unchecked expressions per module, and where Any is still the honest annotation.
- 4
TypedDict, dataclass and BaseModel
Three ways to write a shape down, at three prices. A TypedDict describes a dictionary and costs nothing at runtime, a dataclass generates a real class with attributes and validates nothing, and a pydantic BaseModel checks and coerces every field on construction, turning a quoted 3 into the integer 3 under v2 lax defaults. This chapter matches each to the seam it belongs at, and settles when dict[str, Any] is still the right signature.
- 5
Validation errors at an input boundary
A ValidationError raised where untrusted data enters a program is an expected outcome with a rate, not an incident. Its rendered message carries the value that failed, so logging str(e) writes payload fragments — customer API keys included — into a shared log store, while its errors() list carries field paths and stable codes that aggregate safely. This chapter covers taking the error object apart, logging the shape and returning the value to its sender, and breaking a parse-failure rate down by field before deciding whether a retry, a schema change or a prompt change is the fix.
- 6
Reproducing a notebook result
A notebook's saved outputs record what some kernel session produced, not what the file computes: cells run out of order and variables survive edits, so a committed notebook can show a chart no top-to-bottom run can reproduce. This chapter covers reading execution_count to see the real execution order, why hidden state is a design property rather than a defect, how to attempt a reproduction before automating anything, and the four smallest changes — an imported module, an asserting final cell, nbconvert in CI, and a cache that makes the run fast enough to bother with — that make a monthly number defensible.
- 7
Reading a chained traceback
A chained traceback prints the newest exception last, so the line read first is usually the symptom rather than the cause. This chapter reads the blocks in the order they happened, separates implicit chaining inside an except block from an explicit raise-from, and locates the call boundary — the last of your own frames before the stack descends into a library. It also covers keeping the chain alive as far as the log, and shrinking a 38-minute job into a reproduction that runs in a second.
External resources
- Pydantic documentationdocs.pydantic.dev
Canonical reference for annotation-driven validation and serialization.
- Pydantic AI: Build Type-Safe LLM Agents in Pythonrealpython.com
Build a typed agent end to end with models, tools and validated outputs.
- Type Hints and Pydantic (AI Engineering track)algomaster.io
Why type hints plus Pydantic are the contract layer for LLM I/O.
- Pydantic Crash Course - Build Reliable Python & AI ApplicationsDave Ebbelaar
90 min run through nested models and structured LLM output.