Type safety is the oldest promise in the pydantic ecosystem, and Pydantic AI — subtitled simply “How Python does AI” — is that promise carried into the agent era. The README’s pitch is that an agent is a normal Python object with a model string, an output type, and some tools: write Agent(‘openai:gpt-6-sol’, output_type=Sentiment), decorate a function with @agent.tool, and every run returns a validated Sentiment while your IDE, your type checker, and the model itself all agree on the schema. The same agent object runs in a terminal through the clai CLI, behind a web frontend, on a voice call through realtime support, inside a durable Temporal workflow where every model call survives restarts, or in GitHub Actions. Every model is a string swap away — the catalog resolves names for OpenAI, Anthropic, Google, Bedrock, Groq, Mistral, Cohere, Hugging Face, Ollama, xAI, and more — and image generation and embeddings come in the same box.
What makes the repository unusual is that it is really several disciplined packages under one roof. pydantic_ai_slim carries the SDK itself, with the Agent class and a run loop literally built on a graph engine. pydantic_graph is a standalone typed state machine you can use without any LLM involved, and pydantic_evals applies pytest thinking to agents through datasets, evaluators, and LLM-as-a-judge reports. Beside them, the Harness package snaps capabilities onto any agent — memory, guardrails, planning, compaction, sub-agents — up to a complete Coder agent composed from a filesystem toolset, an allowlisted shell, and repo context. As always in this series, what follows is an educational tour of published source code.
Pydantic AI at a glance: the Agent class drives a run loop built on Pydantic Graph, exchanging typed messages, executing toolsets, validating structured output, and requesting completions through the model catalog; MCP servers feed extra tools in, while Pydantic Evals tests the whole thing and the AI Harness snaps capabilities on top.
Reading the overview from left to right:
- The center of gravity is the Agent class, four and a half thousand lines covering construction through run preparation.
- The abstract interface defines the contract and realtime voice support.
- The run loop expresses one model request plus tool execution as a graph node.
- Conversations are typed message parts, four thousand lines of them.
- Tools come from the function toolset and its composable siblings.
- The model catalog resolves model strings, with the OpenAI adapter the largest single file.
- Pydantic Graph and Pydantic Evals stand beside the SDK.
- The Harness exports the capability library, including a coding agent.
Why You Need This
The first reason is the typing discipline, which is not decoration but the load-bearing structure. The Agent class in pydantic_ai_slim/pydantic_ai/agent/init.py is generic over both dependency and output types, so Agent(DepsT, OutputDataT) fails to type-check before it ever fails at runtime. Tool functions declare their context parameter and the framework generates JSON Schema from the rest of the signature — the tool definition module does this from docstrings and annotations — and arguments are validated before your code executes. Structured output is handled by the output module in two modes: native provider-level JSON schemas or a special tool the model calls to answer. Even the conversation itself is typed — the message module enumerates user prompts, tool calls, retry prompts, thinking parts, and provider-specific details as distinct classes, so streaming code never parses raw dicts.
The second reason is the architecture of the run loop, which is a graph in the most literal sense. The agent graph module defines GraphAgentState, the dependency object, and an AgentNode whose step issues one model request, executes whatever tool calls come back, appends the parts to history, and returns either itself for another round or a final result — the whole agent loop in one node that Pydantic Graph executes. That graph package is a genuine standalone library: nodes return their successors, a decision primitive branches on outcomes, and runs can be persisted and iterated. Around the loop, tools compose through a small algebra — the tool manager coordinates combined toolsets, deferred-loading toolsets that list stubs and load schemas on demand, and approval-required toolsets that gate risky calls behind humans — while the MCP client speaks the Model Context Protocol to external servers so their tools join the pool.
The third reason is the breadth of the model layer and the durability story. The model catalog is nearly twenty-five hundred lines of name resolution feeding adapters for over twenty providers, from the OpenAI adapter at five thousand seven hundred lines to Anthropic at four thousand — and every adapter is wrapped by the instrumented model for OpenTelemetry tracing, with fallback models chaining providers on failure and a deterministic TestModel for keyless unit tests. For work that must survive crashes, the Temporal agent replays every model and tool call as durable activities — Prefect and DBOS variants exist in the same durable_exec directory. And when you need a full assistant rather than a loop, the Harness ships it: the Coder capability composes a filesystem toolset with ripgrep search, an allowlisted shell, repo orientation, planning, memory, and compaction into a terminal coding agent you can run through clai without writing code.
The detail view: the agent tier with wrapper and spec serialization at the top, the run tier with state and streaming, the tool tier from schema generation through capability protocol, the model tier with instrumentation and fallbacks, structured output validation, the Pydantic Graph engine, Pydantic Evals, and the Harness with durable execution.
Walking the detail diagram, the tool tier repays close reading. The capability protocol is the hinge: a capability can contribute toolsets, hooks, and message processing to any agent, which is how the web search capability and the dozens of Harness features attach uniformly. The Harness tree in src shows the economics — memory backed by a store of nearly a thousand lines, compaction with summarizing and sliding-window strategies, sub-agents with their own toolset and task management, planning with a persistable store, guardrails with a detector library, spend tracking with budgets, and step persistence across Postgres, MongoDB, and Redis. A code-mode toolset even runs model-written Python inside Monty, the team’s sandboxed interpreter.
The sibling packages complete the story. Pydantic Evals treats an agent as a unit under test: the dataset module holds cases and evaluators, and the LLM-as-a-judge evaluator scores outputs with another model when hand-written assertions cannot. The spec module serializes an agent’s configuration into a validated model so agents can travel through config files and UIs, and the wrapper base class is what Temporal, Prefect, and DBOS all extend to wrap existing agents without touching their definitions.
From Install to a Typed Answer
The install is one command — uv add pydantic-ai — and the program shape is: construct an Agent with a model string and an output type, attach tools with decorators, and call run_sync or await run; iterate the same run for streaming events or finish with a fully validated output object. Swap the model by changing a string, add web search or image generation as capabilities, point at MCP servers for external tools, and hand the same agent to clai for a terminal session or to Temporal for durable background work. Testing comes free: TestModel fakes deterministic responses, and an Evals dataset holds the regression cases. Structured output, usage limits, retries, and OpenTelemetry tracing are on by default rather than bolted on.
Honest limits: the abstractions lean Python-heavy — generics, dataclass-driven schemas, and typed parts reward teams already fluent in pydantic, but read as ceremony if you just want a quick script. The capability and toolset algebra is powerful enough that debugging which toolset served a given call takes getting used to, and the surface area moves quickly, with Harness features still marked experimental in places. Provider parity varies — thinking blocks, native structured output, and realtime voice all depend on what each API supports. But as a study in how to make LLM applications type-safe without giving up flexibility — typed messages, schema-generated tools, a run loop that is honestly a graph, and evaluation as a first-class citizen — pydantic/pydantic-ai is the reference implementation in this series. Enjoyed this post? Never miss out on future posts by following us