In This Guide
Key Takeaways
- Pydantic AI is a lightweight, type-safe agent framework from the Pydantic team — Samuel Colvin and the same people who built the validation library powering FastAPI
- It uses Pydantic models as the contract between your code and the LLM — every result is parsed and validated against a schema, every tool argument is type-checked
- It is model-agnostic: OpenAI, Anthropic, Gemini, Groq, Mistral, Cohere, and Ollama-style local endpoints all work behind the same
Agentinterface - The core API is small —
Agent,@agent.tool,RunContext,agent.run,agent.run_sync,agent.run_stream— and reads like normal Python rather than a DSL - Dependency injection (
deps_type) is first-class: pass database connections, HTTP clients, or user context into tools without globals - Pydantic Logfire integration gives you OpenTelemetry-compatible tracing of every model call, tool call, and validation step out of the box
What Is Pydantic AI?
Pydantic AI is an open-source Python agent framework built by the Pydantic team — the same group that built Pydantic itself, the validation library that quietly powers FastAPI, the OpenAI Python SDK, the Anthropic SDK, and a substantial fraction of every Python LLM app shipped in the last three years. It is a small, opinionated library for building production-grade LLM agents with full type safety on inputs, tool calls, and structured outputs.
The thesis is simple. If Pydantic models already define the shape of your API requests, your database rows, and your function signatures, those same models should define the shape of what your LLM produces. An agent run is just a function call with a return type — only the function happens to be an LLM and the return type happens to be parsed out of unstructured text. Pydantic AI makes that round-trip type-safe end to end.
The library entered public release in late 2024 and has iterated steadily since. Samuel Colvin, the creator of Pydantic, leads the project. By April 2026 it has become a serious option in the Python agent ecosystem — not the most expansive framework, not the flashiest, but the one a working Python developer is most likely to read and immediately understand.
Why Pydantic AI in 2026
Pydantic AI is positioning itself as "the FastAPI of AI" — a framework that takes a sprawling, ad-hoc problem space and gives Python developers a small set of well-shaped primitives that feel like the rest of the language.
The comparison is intentional. FastAPI did not invent the Python web framework — Flask and Django had been around for a decade. What FastAPI did was take Pydantic models, async, and type hints and weld them into a developer experience that felt obvious in hindsight. You wrote a function with typed parameters, and FastAPI handled validation, OpenAPI docs, and routing. Pydantic AI applies the same instinct to LLM agents.
The friction it removes is real. Most LLM bugs in production fall into a small set of categories: the model returned a string when you expected JSON, the JSON parsed but a required field was missing, a tool was called with arguments of the wrong type, a downstream function got handed None when it expected a string. Every one of those failure modes lives in the gap between the LLM's loose probabilistic output and the strict deterministic typing your application needs. Pydantic AI closes the gap by making that conversion the framework's job, not yours.
The 2026 trend Pydantic AI is riding: agents are moving from notebook prototypes to production services, and production services demand schemas, contracts, and type-safe boundaries. The teams shipping working agents in 2026 are not the ones who chained ten LangChain primitives in a notebook — they are the ones who treat the agent as one more typed function in a Python service. Pydantic AI is built for that team.
Core Concepts
Pydantic AI has a small surface area. Five concepts cover most of what you will write.
The Agent Class
An Agent is the central object. You create one with a model identifier, an optional system prompt, an optional dependency type, and an optional result type. The agent is the thing you call to run the model. A single Agent instance can be reused across many runs, which makes it cheap to create at module level and pass around your service.
Result Types
Pass a Pydantic model as result_type= and the agent's job is to produce an instance of that model. Pydantic AI handles the prompting, the function-calling protocol, the parsing, and the validation. If the model returns something that does not match the schema, Pydantic AI re-prompts with a validation error, up to a configurable retry limit. What you get back is a typed object — your IDE knows its fields, mypy checks them, and your downstream code does not have to defensively unpack a dictionary.
Tools
Tools are Python functions the agent can call. Decorate them with @agent.tool (with context) or @agent.tool_plain (no context). Pydantic AI inspects the function signature and turns it into a JSON Schema the LLM can target. Arguments are validated against that schema before your function ever runs. If the model hallucinates a non-existent argument or the wrong type, Pydantic AI rejects the call with a structured error and re-prompts.
Dependency Injection via RunContext
Tools that need external resources — a database connection, an HTTP client, an authenticated user, a request ID — receive them through RunContext. You declare a deps_type on the agent, pass an instance of that type to agent.run(..., deps=...), and every tool decorated with @agent.tool can read ctx.deps with full typing. No globals, no thread-local hacks, no monkey-patching.
Streaming
Use agent.run_stream to stream output as it is produced. For unstructured text this is a token stream. For structured output this is partial validation — Pydantic AI streams the JSON as it arrives, validates partial-models on the fly, and yields you progressively-complete instances. That last capability is unusual and worth remembering — it is one of the things that makes Pydantic AI feel different from older frameworks.
Installation & First Agent
Installation is one line. The first agent is about ten.
Step 1: Install
# Core install pip install pydantic-ai # Or pin a single provider's deps pip install "pydantic-ai-slim[openai]" pip install "pydantic-ai-slim[anthropic]" pip install "pydantic-ai-slim[groq]"
The pydantic-ai meta-package pulls in clients for every supported provider. The pydantic-ai-slim variant is for production deployments where you want to install only the providers you actually use.
Step 2: A First Agent with Structured Output
from pydantic import BaseModel, Field from pydantic_ai import Agent class Invoice(BaseModel): vendor: str = Field(description="Legal name of the vendor") invoice_number: str total_usd: float = Field(ge=0) line_items: list[str] agent = Agent( 'openai:gpt-4o', result_type=Invoice, system_prompt=( "Extract the structured invoice from the user's message. " "If a field is missing, infer the most likely value." ), ) result = agent.run_sync( "Invoice 4471 from Acme Tooling LLC, $1,240.00 total, " "covering one steel jig and two days of labor." ) print(result.data) # Invoice(vendor='Acme Tooling LLC', invoice_number='4471', # total_usd=1240.0, line_items=['steel jig', 'two days of labor']) print(type(result.data)) # <class '__main__.Invoice'>
Three things to notice. First, you never wrote a JSON-parsing line — Pydantic AI handles the function-calling protocol with the model and parses the result for you. Second, the return value is a real Invoice instance with full IDE autocomplete and mypy support. Third, the Field(ge=0) constraint means a hallucinated negative total is caught at validation time, before your downstream code ever sees it.
Step 3: Async and Streaming Variants
The agent has three run methods. agent.run_sync(prompt) is the synchronous version shown above. agent.run(prompt) is the async coroutine version for use inside async services. agent.run_stream(prompt) returns a context manager whose async iterator yields partial results as the model produces them. All three accept the same arguments and return the same shape of result — you pick the one that fits your runtime.
Tools, Dependencies, & Type-Safe Function Calling
Tools are how an agent does anything beyond producing text. Pydantic AI's tool model is decorator-based, dependency-injected, and fully typed.
A Tool With Dependencies
from dataclasses import dataclass from pydantic import BaseModel from pydantic_ai import Agent, RunContext # 1. Declare what the agent has access to at runtime. @dataclass class SupportDeps: customer_id: str db: "Database" # your async DB client # 2. Declare what the agent must produce. class SupportReply(BaseModel): message: str escalate: bool refund_cents: int = 0 # 3. Wire them into the Agent. support_agent = Agent( 'anthropic:claude-sonnet-4-6', deps_type=SupportDeps, result_type=SupportReply, system_prompt=( "You are a calm, accurate support agent. Use the customer " "lookup tool before issuing any refund. Never refund more " "than the customer's last order amount." ), ) @support_agent.tool async def customer_balance(ctx: RunContext[SupportDeps]) -> float: """Return the customer's current account balance in USD.""" return await ctx.deps.db.balance_for(ctx.deps.customer_id) @support_agent.tool async def last_order_total(ctx: RunContext[SupportDeps]) -> float: """Return the dollar total of the customer's most recent order.""" return await ctx.deps.db.last_order_total(ctx.deps.customer_id) async def handle_ticket(customer_id: str, message: str, db) -> SupportReply: deps = SupportDeps(customer_id=customer_id, db=db) result = await support_agent.run(message, deps=deps) return result.data
Four design moves to notice. The deps_type means tools can read ctx.deps.db with full typing — your editor autocompletes balance_for. The result_type means the surrounding service does not have to guess at what the agent returned — it gets a typed SupportReply. Tool docstrings are shipped to the LLM as tool descriptions, so they double as documentation and as instructions to the model. And because tools are just decorated Python functions, they are unit-testable without ever invoking an LLM.
Pro Tip: Use RunContext for everything that should not be a global
The dependency-injection pattern is the single feature that separates throwaway agents from production agents. Database connections, HTTP clients, the current user, request-scoped tracing IDs, feature flags, tenant context — anything that varies per request goes into your deps dataclass. The agent stays a module-level singleton; the per-request state flows in through RunContext. This is the same pattern FastAPI taught a generation of Python developers, and Pydantic AI lets you keep using it inside the agent.
Tool Validation, Retries, and Errors
If a tool raises a ModelRetry exception, Pydantic AI feeds the message back to the model and asks it to try again. This is how you handle "the model called the tool with a customer ID that does not exist" — you raise ModelRetry("No customer found with that ID. Ask the user to confirm.") and the agent will course-correct. Other exceptions propagate normally and end the run.
Multi-Agent Systems & Streaming
Pydantic AI supports multi-agent workflows by composition. Agents call other agents the same way they call tools — through a typed function that happens to invoke a different model.
Delegating Between Agents
The pattern is straightforward. A "router" or "orchestrator" agent has a tool that calls a specialist agent. The specialist returns a Pydantic model. The orchestrator decides what to do with it. Because each agent is just a Python object, there is no special framework concept for delegation — it is regular function composition. A research agent might delegate to a search agent and a summarizer agent; a support agent might delegate to a refund-policy agent. Each handoff is a typed boundary.
Streaming Structured Output
import asyncio from pydantic import BaseModel from pydantic_ai import Agent class Outline(BaseModel): title: str sections: list[str] writer = Agent('google-gla:gemini-1.5-pro', result_type=Outline) async def stream_outline(): async with writer.run_stream("Outline a guide to FastAPI") as result: async for partial in result.stream(): # `partial` is a partial Outline — fields populate as JSON arrives print(partial) asyncio.run(stream_outline())
The interesting part is what partial contains at each step. Pydantic AI parses the JSON stream incrementally and validates each partial model. Early in the stream you might see Outline(title='Building APIs with FastAPI', sections=[]); a few hundred tokens later you might see Outline(title='Building APIs with FastAPI', sections=['Setup', 'Routes']); finally the full structure. This is genuinely useful for UIs that want to render fields as they arrive without waiting for a complete response.
Model-Agnostic by Design
The model identifier is a string. 'openai:gpt-4o', 'anthropic:claude-sonnet-4-6', 'google-gla:gemini-1.5-pro', 'groq:llama-3.3-70b-versatile', 'mistral:mistral-large-latest', 'cohere:command-r-plus', and any OpenAI-compatible endpoint via OpenAIModel with a custom base URL — Ollama, vLLM, LiteLLM, OpenRouter — all sit behind the same Agent interface. Switching providers is usually a one-line change to the model string. Your tools, your dependencies, and your result types do not move.
Pydantic AI vs LangChain vs LangGraph vs Instructor
The four libraries occupy different positions in the Python LLM stack. None of them is a strict replacement for any other — picking the right one starts with knowing what each was built to solve.
| Capability | Pydantic AI | LangChain | LangGraph | Instructor |
|---|---|---|---|---|
| Primary purpose | Type-safe agents | LLM toolkit / ecosystem | Stateful agent graphs | Structured output |
| Surface area | Small | Very large | Medium | Tiny |
| Type safety on results | First-class | Optional | Optional | First-class |
| Tool calling | Yes (decorator + Pydantic) | Yes | Yes | No (output-focused) |
| Multi-agent / graph workflows | Composition only | Limited | Best-in-class | No |
| Dependency injection | Built-in (RunContext) | Manual | State-based | N/A |
| Streaming structured output | Partial-model streaming | Token streaming | Token streaming | Partial-model streaming |
| Observability | Logfire / OTel | LangSmith | LangSmith | BYO |
| Best fit | Production agents in Python services | Prototyping & ecosystem reach | Long-running stateful workflows | Just-give-me-typed-output |
The decision framework that holds up in practice: reach for Instructor when all you need is "turn this LLM call into a typed object" — no tools, no agentic loop. Reach for Pydantic AI when you need an agent that calls tools, takes dependencies, and returns typed results, and you want it to read like normal Python. Reach for LangGraph when the workflow genuinely is a graph — branching, looping, human-in-the-loop checkpoints, durable state. Reach for LangChain when you need access to its enormous integration library and are willing to accept the surface-area cost.
A common pattern in 2026 is to combine them. Many teams use Pydantic AI for the agent loop, Instructor-style typed extraction for narrow LLM calls inside that loop, and LangGraph only for the orchestration layer when a workflow becomes genuinely state-driven.
Testing & Evals with Pydantic AI
Agents are notoriously hard to test because every call costs money, takes seconds, and produces non-deterministic output. Pydantic AI gives you tools to test agent logic without calling a model at all, and to evaluate model behavior systematically when you do.
Unit Testing With TestModel and FunctionModel
Pydantic AI ships test models that conform to the same interface as a real provider model but produce deterministic output you control. TestModel generates plausible structured data for any result type by inspecting the schema — useful for testing the surrounding code without caring what the LLM said. FunctionModel lets you provide a function that takes the messages and returns a response, which lets you simulate any model behavior — calling specific tools, returning specific structured results, raising errors.
from pydantic_ai import models from pydantic_ai.models.test import TestModel from support_agent import support_agent, SupportDeps def test_handles_basic_question(fake_db): deps = SupportDeps(customer_id="c_42", db=fake_db) # Override the agent's model for the duration of the test. with support_agent.override(model=TestModel()): result = support_agent.run_sync( "Where is my refund?", deps=deps, ) assert isinstance(result.data.message, str) assert result.data.escalate in (True, False)
The agent.override() pattern makes the test path completely deterministic. No network calls, no token cost, no flakiness from a non-deterministic model. The test exercises your tool wiring, your dependency injection, and your result-type validation — all the parts of the agent that are your code, not the LLM's.
Evals
For the parts that are the LLM, Pydantic AI integrates with eval harnesses through Pydantic Evals — a separate package built by the same team — that lets you run an agent against a labeled dataset and score the results. You define cases (input plus expected output or expected behavior), evaluators (functions that score a single case), and the harness reports aggregate metrics. The pattern is familiar to anyone who has used pytest, just at the dataset level rather than the assertion level.
Logfire and Observability
Pydantic Logfire is the Pydantic team's observability product, and Pydantic AI integrates with it natively. Add logfire.configure() at app startup and every agent run, every tool call, every model call, and every validation error becomes a structured trace span. Because Logfire emits OpenTelemetry, you can also wire Pydantic AI traces into Datadog, Honeycomb, Grafana, or any OTel-compatible backend — the integration is not Logfire-locked. For production agents, this is the difference between "we have an agent in prod" and "we know what our agent is doing in prod."
Best Practices & When to Use It
Pydantic AI is at its best when the workload looks like a typed function call dressed up as an agent. Below is the honest playbook — when to reach for it, when to reach for something else.
Use Pydantic AI When
- You are shipping a Python service that calls an LLM. Pydantic AI fits naturally inside FastAPI, Litestar, or plain async services. The same Pydantic models you use for request and response schemas can be used for agent results.
- Your agent calls a small set of typed tools and returns a typed result. This is the sweet spot — single-agent or small multi-agent workloads with clear contracts.
- You want type safety to catch bugs at edit time. If your team uses mypy or Pyright in CI, Pydantic AI plays cleanly with both. The IDE feedback loop is genuinely faster.
- You want provider portability. Switching from GPT-4o to Claude Sonnet to Gemini to a self-hosted Llama is a string change. That matters when pricing, rate limits, or capabilities shift.
- You want observability without writing it yourself. Logfire integration via OpenTelemetry is one line and gives you full traces.
Reach For Something Else When
- Your workflow is genuinely a graph. Long-running, branching, human-in-the-loop, with checkpoints and durable state — that is LangGraph's home turf, and trying to reproduce it in Pydantic AI by composition will hurt.
- You need a specific integration LangChain already wraps. Vector stores, document loaders, retrievers, exotic API connectors — LangChain's ecosystem is unmatched. Use it for that, then call into Pydantic AI for the typed agent layer if you want both.
- All you need is structured output from a single LLM call. Instructor or the providers' native structured-output APIs are simpler than spinning up an Agent. Reserve Pydantic AI for when you actually want tool calling and dependency injection.
The Production Playbook
Six habits that separate Pydantic AI experiments from Pydantic AI in production. One: create the Agent at module level, never per-request — agents are cheap to create but creating them per-request is wasted work. Two: always declare a result_type, even if it is just a single-field model — strings as agent outputs are a code smell. Three: always declare a deps_type, even if you do not use it yet — adding it later is a breaking change to every tool. Four: raise ModelRetry for recoverable tool errors and let normal exceptions propagate for unrecoverable ones — the model will course-correct on the first, fail loudly on the second. Five: wire Logfire from day one — debugging an agent without traces is brutal, and the pricing is friendly to small teams. Six: write TestModel-based unit tests for your tool wiring and Pydantic Evals for your model behavior — the two test types catch different bugs and you need both.
Sources: Pydantic AI Documentation, pydantic/pydantic-ai on GitHub, Pydantic Logfire. API surface verified against pydantic-ai releases through April 2026; model identifiers and pricing are subject to change as providers iterate.
Explore More Guides
Pydantic AI is not yet LangChain-sized — and that is precisely why working Python teams are picking it.
The honest read on adoption in April 2026: LangChain is still the default Python LLM library by raw download volume, by tutorial count, and by integration breadth. If you searched "Python LLM agents" eighteen months ago, every answer pointed to LangChain. That has not collapsed — but it has shifted. Working teams who actually need to ship reliable agents into production services have been quietly moving the agent layer to Pydantic AI, Instructor, or the model providers' native SDKs, while keeping LangChain around for retrievers, vector store wrappers, and integrations they do not want to rebuild. The reason is not that LangChain is bad. It is that LangChain optimized for breadth, and breadth is expensive when the cost is a sprawling abstraction surface that changes underneath you.
Pydantic AI's bet is the opposite — small surface, well-shaped primitives, schema-driven contracts, and the boring discipline of treating the agent like a normal typed function. That bet is paying off in the kind of teams who think about mypy in CI, who run agents inside FastAPI services, and who consider observability a Day-1 problem. Those teams find Pydantic AI immediately legible. A senior Python developer can read the docs in an evening, write a real agent in an afternoon, and have it tested and instrumented by the end of the week. That is genuinely rare in this space.
The fair caution: Pydantic AI is younger and the ecosystem is smaller. There are fewer Stack Overflow answers, fewer YouTube tutorials, and fewer "I built X with Pydantic AI" blog posts than for LangChain. If you hit a weird edge case, the community is smaller and the answer might require reading the source. But the source is small and clean, the maintainers are responsive, and the trajectory is unmistakably upward. For 2026, the right move for most production Python teams is: use Pydantic AI for the agent loop, use Instructor or native APIs for narrow typed extraction, reach for LangGraph only when the workflow is genuinely a graph, and accept LangChain as a useful integration library rather than a framework to live inside.