Build Typed Agents with Pydantic AI in Python (2026) — give every agent run a validated Pydantic result, typed tools, and dependency injection in one Agent API.
You already ship tools with FastMCP and cross-agent calls with Google A2A. This guide is the missing middle: how you write the agent loop itself so outputs are typed end to end.
TL;DR
Agent(..., output_type=MyModel)— every run returns a validated instance@agent.tool— function signature + docstring become the tool schemadeps_type— inject DB/clients safely (great for tests)- Start with the built-in
'test'model offline, then swap a real model string
Install
pip install pydantic-ai
# or: uv add pydantic-ai
Works on current Python 3.10+ (3.13/3.14 fine for these patterns).
Mental model
| Layer | Job | PyInns guide |
|---|---|---|
| Pydantic AI | Typed agent loop + structured results | This post |
| MCP / FastMCP | Tools a host can call | FastMCP servers |
| A2A | Agent-to-agent protocol | A2A in Python |
| LangGraph | Graph orchestration / checkpoints | LangGraph agents |
Example 1 — structured output
Define a Pydantic model and set it as output_type. The agent validates the model reply before you see it.
from typing import Literal
from pydantic import BaseModel, Field
from pydantic_ai import Agent
class Sentiment(BaseModel):
label: Literal["positive", "negative", "neutral"]
score: float = Field(ge=-1, le=1)
# 'test' runs offline — no API key required
agent = Agent("test", output_type=Sentiment)
result = agent.run_sync("How are people feeling about the Extract app?")
print(result.output)
# Sentiment(label='positive', score=0.9) # shape guaranteed
print(result.output.label, result.output.score)
When you are ready for a real provider, change only the model string, for example openai:gpt-5.2 or anthropic:claude-sonnet-4-5 (names move — check current Pydantic AI model docs).
Example 2 — tools the model can call
Decorate a function with @agent.tool. Arguments are validated; the docstring becomes the tool description.
from pydantic_ai import Agent, RunContext
agent = Agent("test", output_type=Sentiment)
@agent.tool
def recent_reviews(ctx: RunContext[None], product: str) -> list[str]:
"""Fetch recent review snippets for a product."""
# replace with your DB / HTTP client
return ["The new release fixed everything I complained about!"]
result = agent.run_sync("How are people feeling about Extract?")
print(result.output)
Example 3 — dependency injection
Pass connections and IDs through deps_type so tools stay pure and tests stay easy.
from dataclasses import dataclass
from pydantic_ai import Agent, RunContext
@dataclass
class ReviewDeps:
product_id: str
agent = Agent("test", deps_type=ReviewDeps, output_type=Sentiment)
@agent.tool
def recent_reviews(ctx: RunContext[ReviewDeps]) -> list[str]:
"""Load reviews for the product in deps."""
pid = ctx.deps.product_id
return [f"Review for {pid}: solid release"]
result = agent.run_sync(
"Summarize sentiment for this product.",
deps=ReviewDeps(product_id="extract-app"),
)
print(result.output)
Pydantic AI vs Instructor vs LangGraph
- Instructor — best when you only need one-shot structured extraction from an existing OpenAI/Anthropic client.
- Pydantic AI — best when you need an agent loop: tools, deps, retries, capabilities (including MCP), typed results.
- LangGraph — best when the workflow is a graph with durable state; you can still call a Pydantic AI agent from a node.
Production tips
- Prototype with
Agent("test"), then swap the model string. - Keep tools small and side-effect aware; validate inputs with types.
- Add observability (OpenTelemetry / Logfire) before you scale traffic.
- Expose host tools via MCP; expose your agent to peers via A2A — do not reinvent either protocol.
Wrap-up
In 2026, typed agents are the default expectation — not freeform strings you parse by hand. Pydantic AI gives you that with familiar Pydantic models. Start with output_type, add one tool, inject deps, then connect MCP and A2A when you ship.