Pydantic AI Tutorial: Build Type-Safe AI Agents (2026)
Pydantic AI is a Python agent framework that brings FastAPI’s developer experience to GenAI — type-safe, model-agnostic, and built for production from day one. If you’ve ever been frustrated by LangChain’s complexity or wanted structured outputs without fighting JSON parsing, Pydantic AI is the framework to try. It’s built by the same team behind Pydantic, the most downloaded Python validation library.
Who this is for:
- Junior engineers: You want a clean, intuitive way to build your first AI agent without learning a massive framework
- Senior engineers: You need type-safe, testable agents with dependency injection and structured outputs for production
Real-World Problem Context
Section titled “Real-World Problem Context”Building AI agents in 2026 means choosing between frameworks that each have painful trade-offs:
| Problem | LangChain | Raw API Calls | Pydantic AI |
|---|---|---|---|
| Structured output from LLMs | OutputParser + retry logic | Manual JSON parsing + validation | Declare a Pydantic model, done |
| Switching between GPT-4o and Claude | Different chain configs | Different API clients + schemas | Change one string: 'openai:gpt-4o' → 'anthropic:claude-sonnet-4-20250514' |
| Testing agents without API calls | Mock the entire chain | Mock HTTP calls | Built-in TestModel — zero API calls, deterministic output |
| Type safety | Runtime errors on bad schemas | No validation until production | Compile-time type checking with mypy/pyright |
| Dependency injection | Not built-in | Manual wiring | First-class deps_type parameter |
“How do I build a Pydantic AI agent with structured output?” — the answer is surprisingly simple: define a Pydantic model, pass it as
output_type, and the framework handles the rest.
The core insight: Pydantic AI treats agent building like API development. You declare inputs, outputs, and dependencies — the framework handles the rest. If you’ve used FastAPI, you’ll feel right at home.
How Pydantic AI Works
Section titled “How Pydantic AI Works”Think of Pydantic AI like FastAPI, but for LLM interactions. In FastAPI, you define an endpoint with typed parameters and a response model. In Pydantic AI, you define an Agent with typed dependencies and an output model.
The Four Building Blocks
Section titled “The Four Building Blocks”-
Agent — The core object. You give it a model name, an output type, and system instructions. It handles the LLM conversation loop, tool calls, and response validation.
-
Output Types — A Pydantic
BaseModelthat defines what the agent must return. The framework generates the JSON schema, sends it to the LLM, and validates the response automatically. No manual parsing. -
Tools — Python functions decorated with
@agent.tool. The framework extracts the function signature, generates a tool schema, and handles the call/response cycle. You write plain Python; the framework does the LLM wiring. -
Dependencies — A typed dataclass passed via
RunContext. This is how you inject database connections, API clients, or user context into tools — without global state.
Unlike LangChain where you compose chains of abstractions, Pydantic AI keeps you close to plain Python. An agent is a class. A tool is a function. An output is a Pydantic model. No chains, no runnables, no LCEL syntax.
Step-by-Step: Build a Structured-Output Agent
Section titled “Step-by-Step: Build a Structured-Output Agent”You’ll build a customer support agent that takes a user message and returns a structured response with advice, risk level, and recommended action.
Step 1: Install Pydantic AI
Section titled “Step 1: Install Pydantic AI”pip install pydantic-aiStep 2: Define Your Output Model
Section titled “Step 2: Define Your Output Model”This is the Pydantic model the agent must return. Every field is validated:
from pydantic import BaseModel
class SupportResponse(BaseModel): advice: str """The support advice to give the customer.""" risk_level: int """Risk level from 1 (low) to 10 (high).""" block_card: bool """Whether to block the customer's card.""" escalate: bool """Whether to escalate to a human agent."""Step 3: Define Dependencies
Section titled “Step 3: Define Dependencies”Dependencies are injected into tools via RunContext. Define them as a dataclass:
from dataclasses import dataclass
@dataclassclass SupportDeps: customer_id: int customer_name: str account_balance: floatStep 4: Create the Agent
Section titled “Step 4: Create the Agent”from pydantic_ai import Agent
support_agent = Agent( "openai:gpt-4o", deps_type=SupportDeps, output_type=SupportResponse, instructions=( "You are a bank support agent. Assess the customer's issue, " "provide clear advice, and rate the risk level. " "Use the customer's name in your response." ),)Step 5: Add Tools
Section titled “Step 5: Add Tools”Tools let the agent access external data. The RunContext parameter gives you typed access to dependencies:
from pydantic_ai import RunContext
@support_agent.toolasync def get_recent_transactions(ctx: RunContext[SupportDeps]) -> str: """Retrieve the customer's recent transactions.""" # In production, this would query your database return f"Last 3 transactions for customer {ctx.deps.customer_id}: $50 grocery, $200 electronics, $15 coffee"
@support_agent.toolasync def check_fraud_alerts(ctx: RunContext[SupportDeps]) -> str: """Check if there are any fraud alerts on the account.""" return f"No fraud alerts for customer {ctx.deps.customer_id}"Step 6: Run the Agent
Section titled “Step 6: Run the Agent”import asyncio
async def main(): deps = SupportDeps( customer_id=123, customer_name="Alice", account_balance=1500.00 )
result = await support_agent.run( "I just noticed a $200 charge I didn't make!", deps=deps )
# result.output is a validated SupportResponse print(f"Advice: {result.output.advice}") print(f"Risk: {result.output.risk_level}") print(f"Block card: {result.output.block_card}") print(f"Escalate: {result.output.escalate}") print(f"Tokens used: {result.usage()}")
asyncio.run(main())The output is a fully validated SupportResponse object — not a string, not a dict, but a type-safe Pydantic model.
Pydantic AI Architecture and Design
Section titled “Pydantic AI Architecture and Design”Pydantic AI processes requests through four sequential stages: input assembly, the agent orchestration loop, tool execution, and Pydantic model validation of the final output.
📊 How Pydantic AI Processes a Request
Section titled “📊 How Pydantic AI Processes a Request”Pydantic AI Agent Execution Flow
From user prompt to validated structured output
📊 The Pydantic AI Stack
Section titled “📊 The Pydantic AI Stack”Pydantic AI Architecture
Clean separation between your code and framework internals
The key architectural insight: Pydantic AI separates what your agent does (your code at the top) from how it communicates with LLMs (the framework layers below). Switching from GPT-4o to Claude is a one-line change because the model interface abstracts provider differences.
Pydantic AI Code Examples in Python
Section titled “Pydantic AI Code Examples in Python”These three patterns demonstrate Pydantic AI’s most distinctive capabilities: model-agnostic switching, deterministic CI testing with TestModel, and dynamic system instructions from dependencies.
Model-Agnostic Agents
Section titled “Model-Agnostic Agents”One of Pydantic AI’s strongest features — switch models without changing any agent code:
# Same agent, different modelsagent_gpt = Agent("openai:gpt-4o", output_type=SupportResponse, ...)agent_claude = Agent("anthropic:claude-sonnet-4-20250514", output_type=SupportResponse, ...)agent_gemini = Agent("gemini-2.0-flash", output_type=SupportResponse, ...)agent_local = Agent("ollama:llama3.2", output_type=SupportResponse, ...)Testing Without API Calls
Section titled “Testing Without API Calls”Pydantic AI includes a TestModel that returns deterministic responses — no API keys needed:
from pydantic_ai.models.test import TestModel
async def test_support_agent(): """Test the agent without making any LLM API calls.""" with support_agent.override(model=TestModel()): deps = SupportDeps(customer_id=1, customer_name="Test", account_balance=100) result = await support_agent.run("Test message", deps=deps)
# TestModel returns valid instances of your output type assert isinstance(result.output, SupportResponse) assert 1 <= result.output.risk_level <= 10This is a game-changer for CI/CD pipelines. Your agent tests run in milliseconds, cost nothing, and never flake due to API timeouts.
Dynamic System Instructions
Section titled “Dynamic System Instructions”Add context-aware instructions that read from dependencies:
@support_agent.instructionsasync def add_context(ctx: RunContext[SupportDeps]) -> str: return ( f"The customer's name is {ctx.deps.customer_name}. " f"Their account balance is ${ctx.deps.account_balance:.2f}." )The instruction function runs before every agent call, injecting live context into the system prompt.
Trade-offs and Common Pitfalls
Section titled “Trade-offs and Common Pitfalls”Pydantic AI trades ecosystem breadth for type safety and testability — the right choice depends on whether you need pre-built RAG components or clean, validated agent outputs.
Pydantic AI vs LangChain
Section titled “Pydantic AI vs LangChain”📊 Visual Explanation
Section titled “📊 Visual Explanation”Pydantic AI vs LangChain
- Type-safe structured outputs — validated automatically
- Built-in TestModel for deterministic testing
- Dependency injection via RunContext
- Model-agnostic — one-line model switching
- Smaller ecosystem — fewer pre-built integrations
- No built-in RAG, vector store, or retriever components
- Newer framework — less community content and tutorials
- Massive ecosystem — 700+ integrations
- Built-in RAG, vector stores, retrievers
- Large community — Stack Overflow, Discord, tutorials
- Complex abstraction layers — hard to debug
- No built-in dependency injection
- Structured output requires OutputParser + retry logic
Where Engineers Get Burned
Section titled “Where Engineers Get Burned”Other gotchas:
- Output validation retries — If the LLM returns invalid JSON, Pydantic AI retries automatically. But if your output model is too complex (deeply nested, many optional fields), retries can consume extra tokens. Keep output models flat when possible.
- Async by default —
agent.run()is async. For scripts and notebooks, useagent.run_sync(). Forgetting this causesRuntimeError: cannot be called from a running event loop. - Tool signature matters — Pydantic AI generates tool schemas from your function signatures. Unclear parameter names or missing docstrings produce bad tool descriptions, causing the LLM to misuse tools.
Interview Questions and Answers
Section titled “Interview Questions and Answers”These four questions cover the concepts interviewers use to assess whether you understand Pydantic AI’s design philosophy versus LangChain’s ecosystem approach.
Q1: “What is Pydantic AI and how is it different from LangChain?”
Section titled “Q1: “What is Pydantic AI and how is it different from LangChain?””What they’re testing: Can you articulate the architectural philosophy difference?
Strong answer: “Pydantic AI is a minimal agent framework built by the Pydantic team. It focuses on three things: type-safe structured outputs via Pydantic models, dependency injection via RunContext, and model-agnostic agents. LangChain is an ecosystem with hundreds of integrations — retrievers, vector stores, document loaders. Pydantic AI is lean by design; LangChain is batteries-included.”
Weak answer: “Pydantic AI is newer and simpler.”
Q2: “How does Pydantic AI handle structured outputs?”
Section titled “Q2: “How does Pydantic AI handle structured outputs?””What they’re testing: Do you understand the structured output mechanism vs. parsing JSON strings?
Strong answer: “You pass a Pydantic BaseModel as the output_type parameter. The framework sends the JSON schema to the LLM, the LLM responds with structured JSON, and Pydantic validates it automatically. If validation fails, the framework retries with the error message. You never parse JSON strings manually.”
Q3: “How would you test a Pydantic AI agent in CI without API keys?”
Section titled “Q3: “How would you test a Pydantic AI agent in CI without API keys?””What they’re testing: Production readiness — can you test AI code reliably?
Strong answer: “Pydantic AI has a built-in TestModel that returns valid instances of your output type without calling any LLM API. You use agent.override(model=TestModel()) in tests. This gives you deterministic, fast, free tests that run in CI without API credentials.”
Q4: “When would you choose Pydantic AI over LangChain?”
Section titled “Q4: “When would you choose Pydantic AI over LangChain?””What they’re testing: Can you match the framework to the problem?
Strong answer: “I’d choose Pydantic AI when I need structured outputs with validation, when testability is a priority, when I want to swap models easily, or when the team values type safety. I’d stick with LangChain if I need pre-built RAG components, vector store integrations, or the LangGraph state machine for complex agent orchestration.”
Running Agents in Production
Section titled “Running Agents in Production”Here’s how teams use Pydantic AI in production:
Structured API backends: The most common pattern is using Pydantic AI agents behind a FastAPI endpoint. The agent’s output_type becomes the API response model — one Pydantic model serves both the LLM validation and the HTTP response schema.
Multi-model routing: Teams define one agent with the same tools and output type, then create multiple instances with different models. A router sends simple queries to a cheaper model (GPT-4o mini) and complex queries to a stronger model (Claude Sonnet). Since Pydantic AI is model-agnostic, this takes 3 lines of code.
Observability: Pydantic AI integrates with Logfire (also from the Pydantic team) for tracing. Every agent call, tool execution, and retry becomes a traceable span. For teams already using other observability tools, Pydantic AI also works with OpenTelemetry.
Cost control: The result.usage() method returns exact token counts and costs. Teams log this per-request and set alerts on cost anomalies. Since output models are validated, you don’t burn tokens on retry loops from malformed responses (a common LangChain problem).
For more on how Pydantic AI fits into the broader agent framework landscape, see our comparison guide.
Summary and Key Takeaways
Section titled “Summary and Key Takeaways”- Pydantic AI brings FastAPI’s developer experience to AI agents — type-safe, model-agnostic, and production-first
- Structured outputs are first-class — declare a Pydantic model, pass it as
output_type, get validated objects back - Dependency injection via
RunContextreplaces global state and makes agents testable TestModelenables deterministic testing — no API keys, no cost, no flaky tests in CI- Model-agnostic — switch between OpenAI, Anthropic, Google, or local models with one string change
- Intentionally minimal — no built-in RAG, vector stores, or document loaders. Bring your own or combine with LangChain components
- Best for: Teams that value type safety, testability, and clean architecture over a large pre-built ecosystem
Related
Section titled “Related”- Agentic Frameworks Compared — Pydantic AI vs LangChain vs CrewAI vs AutoGen
- AI Agents — Agent architectures and when to use each
- Agentic Design Patterns — ReAct, tool-use, and multi-agent patterns
- LangChain vs LangGraph — The ecosystem Pydantic AI competes with
- Python for GenAI — Python fundamentals for AI development
Last updated: February 2026 | Pydantic AI v1.x / Python 3.10+
Frequently Asked Questions
What is Pydantic AI and how does it compare to LangChain?
Pydantic AI is a Python agent framework built by the Pydantic team that brings FastAPI's developer experience to GenAI — type-safe, model-agnostic, and production-focused. Compared to LangChain, Pydantic AI has fewer abstractions, uses Pydantic models for validated structured outputs, supports dependency injection for testable code, and works with any LLM provider. It is designed for engineers who want clean, typed agents without a massive framework.
How does Pydantic AI handle structured outputs?
Define a Pydantic BaseModel as the output_type when creating an agent. The framework ensures the LLM returns JSON matching your model's schema, with automatic validation and retry on malformed responses. This guarantees type-safe outputs — every field has the correct type, required fields are present, and the output can be used directly in downstream code without manual parsing or validation.
What is dependency injection in Pydantic AI?
Pydantic AI uses dependency injection to pass runtime context (database connections, user sessions, API clients) to agent tools without global state. Define a deps_type dataclass, pass dependencies when running the agent, and tools access them via RunContext. This makes agents testable (inject mock dependencies in tests) and avoids the global state patterns that make other frameworks hard to test.
When should I use Pydantic AI vs LangGraph?
Use Pydantic AI for straightforward agents with structured outputs, dependency injection, and type safety — especially when you want minimal framework overhead and testable code. Use LangGraph when you need complex state machines with cycles, conditional routing, human-in-the-loop interrupts, and multi-agent orchestration. Pydantic AI is simpler and more Pythonic; LangGraph is more powerful for complex workflows.
How do I get started with Pydantic AI?
Install with pip install pydantic-ai, then create an Agent with a model name, an output_type (a Pydantic BaseModel), and system instructions. Add tools as decorated Python functions with RunContext for dependency access. Call agent.run() with a user message and dependencies. The framework handles the LLM conversation loop, tool calls, and output validation automatically.
What are agent tools in Pydantic AI?
Agent tools are Python functions decorated with @agent.tool that give the agent access to external data and capabilities. Pydantic AI extracts the function signature and docstring to generate a tool schema for the LLM. Tools receive a RunContext parameter with typed access to dependencies, so they can query databases or call APIs without global state. The framework handles the tool call and response cycle automatically.
How do I test Pydantic AI agents without API keys?
Pydantic AI includes a built-in TestModel that returns deterministic, valid instances of your output type without calling any LLM API. Use agent.override(model=TestModel()) in your tests. This enables fast, free, and reliable CI/CD testing — no API keys needed, no flaky tests from API timeouts, and results are deterministic across runs.
Is Pydantic AI model-agnostic?
Yes, Pydantic AI supports OpenAI, Anthropic, Google Gemini, and local models via Ollama through a unified interface. Switching between models is a one-line change — you only modify the model name string. Your agent code, tools, output types, and dependencies remain identical regardless of the provider. See our agentic frameworks comparison for how it stacks up.
What are the limitations of Pydantic AI?
Pydantic AI is intentionally minimal and does not include pre-built RAG components, vector store integrations, or document loaders that LangChain provides. Complex output models with deeply nested structures can cause extra token usage during validation retries. The framework is async by default, and the ecosystem is newer with less community content compared to LangChain.
How is Pydantic AI used in production?
Common production patterns include structured API backends where the agent output_type doubles as the FastAPI response model, multi-model routing that sends simple queries to cheaper models and complex queries to stronger ones, and observability via Logfire or OpenTelemetry for tracing every agent call and tool execution. Teams also use agent architectures that combine Pydantic AI with RAG pipelines for knowledge retrieval.