Advanced Python¶
The bar: you can write production-grade Python that validates its own inputs, catches type errors before runtime, and makes dozens of AI API calls in parallel without falling over.
You already know Python fundamentals — loops, functions, dictionaries. That gets you a script. It does not get you a service that other people depend on. The gap between "works on my machine" and "runs in production" is filled with the patterns in this module: type safety so you catch mistakes before your users do, data validation so bad input never reaches your model, and async so an app waiting on a slow API doesn't waste every second it's given.
This is the toolkit modern AI engineering actually runs on. Every serious Python codebase you'll join uses type hints, Pydantic, and asyncio. Learn them here and you stop writing scripts and start shipping software.
15.1 Modern Python Patterns¶
Objective: write production-quality Python with a clean, reproducible project setup.
Python 3.11+ brought real speed gains and better error messages, and it's the floor for modern AI work. But the language is only half the job — the other half is the environment around it. A production project isn't a folder of loose .py files; it's a defined structure with a locked set of dependencies that installs the same way on your laptop and on a server.
uv is the fast virtual-environment and package manager that has largely replaced the old python -m venv plus pip dance. You declare your dependencies in pyproject.toml — the single source of truth for what your project needs and how it's built — instead of a stray requirements.txt you forgot to update. Formatting and linting are handled by black (opinionated formatting, no arguments about style) and ruff (an extremely fast linter that catches bugs and bad patterns).
The common self-taught mistake here is installing packages globally with pip install and no virtual environment. Six months later nothing reproduces, versions conflict, and you can't tell which package your code actually depends on. Isolate every project from day one.
- Drill: create a project with
uv init, add dependencies withuv add, and run it in an isolated environment withuv run. - Drill: define project metadata and dependencies in
pyproject.tomlinstead ofrequirements.txt. - Drill: format a file with
black .and lint it withruff check .; fix every warning before you commit. - Drill: structure an AI app as a real package — a
src/layout, a clear entry point, and separated modules rather than one giant file.
15.2 Type Hints¶
Objective: add type safety so the tooling catches errors before your users do.
Python runs fine without types, which is exactly why untyped code rots. Type hints are annotations — name: str, count: int — that tell both the reader and the tooling what a value is supposed to be. They cost you a few extra characters and buy you an editor that autocompletes correctly and a checker that flags mismatches before you ship.
Start with primitives and collections (list[str], dict[str, int]), then reach into the typing module for the harder cases: Optional for values that might be None, Union for values that could be one of several types, and generic types for containers that work over any element. Protocol classes let you type by behavior ("anything with a .read() method") instead of by inheritance — structural typing that fits Python's duck-typed nature. TypedDict describes the exact shape of a JSON object, which matters enormously when you're passing model requests and responses around as dictionaries.
mypy is the static analyzer that reads your annotations and tells you where they don't line up. The gotcha: hints are not enforced at runtime — Python won't stop you from passing a string where you promised an int. The type checker is what enforces them, so hints without mypy in your workflow are just documentation. Wire it in.
- Drill: annotate function signatures with primitives and collection generics (
list[str],dict[str, int]). - Drill: use
Optional,Union, and generic types fromtypingfor nullable and polymorphic values. - Drill: define a
Protocolclass and type a function against behavior instead of a concrete class. - Drill: describe a JSON payload with
TypedDict, then runmypyand drive the error count to zero.
15.3 Pydantic v2: Data Validation¶
Objective: use Pydantic to validate and serialize production data with confidence.
Type hints check your code; Pydantic checks your data. A BaseModel turns a class of typed fields into a runtime schema — hand it a dictionary and it either gives you back a validated, typed object or raises a clear error explaining exactly what was wrong. For AI applications, where inputs arrive as loosely structured JSON from users and models, this is the wall that keeps garbage out of your pipeline.
Beyond the basic type checks, you attach real rules. @field_validator enforces custom logic on a single field (a token count must be positive, a role must be one of a known set). @model_validator handles cross-field logic where one field's validity depends on another. @computed_field exposes derived values as if they were stored. When you're done, model_dump() and model_dump_json() serialize the object back out cleanly, and model_json_schema() emits a JSON Schema that plugs straight into OpenAPI docs. BaseSettings reads configuration from environment variables with the same validation guarantees, so a missing or malformed config value fails loudly at startup instead of silently at 3 a.m.
The mistake to avoid: hand-writing if not isinstance(...) checks scattered through your code. That's the exact job Pydantic does declaratively and completely. If you find yourself validating a dict by hand, stop and model it.
- Drill: define a
BaseModelwith typed fields and instantiate it from a raw dict. - Drill: add
@field_validatorfor single-field rules and@model_validatorfor cross-field rules. - Drill: expose a derived value with
@computed_field; serialize withmodel_dump()andmodel_dump_json(). - Drill: load config with
BaseSettingsand generate a schema withmodel_json_schema().
15.4 Async Python¶
Objective: write asynchronous code that keeps I/O-bound AI apps fast.
AI apps spend most of their life waiting — on model APIs, databases, HTTP calls. Synchronous code waits on each one in sequence, wasting the whole trip. async/await lets a single thread start a slow operation, go do other work while it's pending, and pick it back up when the result lands. For I/O-bound work, this is the difference between an app that handles one request at a time and one that handles hundreds.
asyncio.gather is the workhorse: fire off many API calls at once and wait for all of them together, instead of one after another. httpx is the async-capable HTTP client you'll use to make those calls (the async cousin of requests). AsyncIterator lets you stream results as they arrive — essential for token-by-token model output where you don't want to wait for the full response before showing anything. And because parallel calls will hammer any real API into a rate-limit error, you pair them with rate limiting and exponential backoff so a burst of 429 responses becomes a brief, automatic retry instead of a crash.
The classic trap: calling a blocking, synchronous function inside an async one. It freezes the entire event loop and quietly kills all your concurrency. If a library has an async API, use it; don't mix a blocking call into the middle of your async path.
- Drill: write
async deffunctions andawaitthem correctly from an event loop. - Drill: run many calls in parallel with
asyncio.gatherand compare wall-clock time to a sequential loop. - Drill: make async HTTP requests with
httpxand stream a response with anAsyncIterator. - Drill: add rate limiting and exponential backoff so bursts survive
429responses.
15.5 Hands-On Lab: Pydantic Schema Library¶
Objective: build a type-safe library of data models for an AI application.
What you ship
A small, installable Pydantic v2 schema library for an AI chat application. Deliverables:
- Models for chat messages and completions — typed fields, validated on construction.
- Token-limit validators (
@field_validator/@model_validator) that reject payloads exceeding a configured maximum before they ever reach a model. - A
BaseSettings-backed configuration layer that reads limits and API config from the environment and fails loudly when something's missing. - OpenAPI-compatible schemas emitted via
model_json_schema(), ready to document an API surface.
Ship it fully type-annotated, mypy-clean, and formatted with black and ruff.
On the VWC codebase
The Vets Who Code app is TypeScript on Next.js (Pages Router), not Python — so you won't drop this library into it directly. But the discipline transfers one-to-one. Pydantic's "validate at the boundary" is exactly what you'd enforce in a src/pages/api handler before touching the database: parse and check the request body, reject bad input with a clear error, and only then act. The same instinct that makes you model a payload with a BaseModel in Python makes you validate and type it in a TypeScript API route instead of trusting whatever arrives.