Most agent bugs aren't reasoning failures. They're shape failures: the model returns prose where your code expected an object, a string where you needed an integer, or a required field that quietly went missing. Pydantic AI closes that gap. You install the package, point an Agent at a model, declare a Pydantic output_type, and the framework validates every response and asks the model to correct itself when the shape is wrong. This guide walks the full procedure, plus the tool and dependency-injection patterns that make these agents testable.
Key Takeaways
- Short answer: run
pip install pydantic-ai, createAgent('openai:gpt-5.2', output_type=YourModel), callagent.run_sync(prompt), and read the validated result offresult.output. - Pydantic AI validates every model response against your
BaseModeland retries automatically when validation fails; the output retry budget defaults to one attempt and is configurable. - Tools are plain Python functions registered with
@agent.tool. The schema the model sees is generated from your function signature and docstring, so naming and types matter. - Runtime dependencies (database clients, API keys, config) are injected type-safely through
deps_typeand read inside tools viactx.deps. - Model identifiers use a
provider:modelstring, so you can swap OpenAI, Anthropic, or Gemini without touching agent logic.
What You Need Before You Start
- Python 3.10 or newer, ideally in a fresh virtual environment.
- An API key for one provider (OpenAI, Anthropic, or Google), exported as an environment variable.
- Working knowledge of Pydantic
BaseModeland Python type hints. The whole framework leans on them.
How Do You Build a Typed Pydantic AI Agent?
Follow these seven steps end to end. The first three get a working agent in front of you in under a minute; the rest add the structure you'll actually ship.
-
Install the package. The default install pulls in OpenAI, Anthropic, and Google support:
pip install pydantic-aiIf you want a lean dependency tree, install the slim variant and name only the providers you use:
pip install "pydantic-ai-slim[openai,logfire]" -
Set your provider API key. Pydantic AI reads it from the environment. For OpenAI:
export OPENAI_API_KEY=sk-...Use
ANTHROPIC_API_KEYorGEMINI_API_KEYinstead if you point the agent at those providers. -
Write a minimal agent and run it. This confirms your key and install before you add any structure:
from pydantic_ai import Agent agent = Agent( 'openai:gpt-5.2', instructions='Be concise, reply with one sentence.', ) result = agent.run_sync('Where does "hello world" come from?') print(result.output)The model string is
provider:model. Swap inanthropic:claude-sonnet-4-6orgoogle:gemini-3-flash-previewand nothing else changes. -
Declare a typed output. Replace the free-text reply with a Pydantic model, and pass it as
output_type. Now the agent is forced to return data matching this schema:from pydantic import BaseModel, Field from pydantic_ai import Agent class SupportOutput(BaseModel): advice: str = Field(description="Advice to give the customer") block_card: bool = Field(description="Whether to block their card") risk: int = Field(description="Risk level of the query", ge=0, le=10) agent = Agent( 'openai:gpt-5.2', output_type=SupportOutput, instructions=( "You are a support agent at a bank. Judge the risk of the " "customer's query and decide whether to block their card." ), ) -
Define your dependencies. Group the runtime objects the agent needs (a database handle, an HTTP client, IDs) into a dataclass, and tell the agent about it with
deps_type:from dataclasses import dataclass @dataclass class SupportDeps: customer_id: int db: "DatabaseConn" # your own async DB wrapper agent = Agent( 'openai:gpt-5.2', deps_type=SupportDeps, output_type=SupportOutput, instructions="You are a support agent at a bank.", ) -
Register a tool. Any function the model may call becomes a tool. Take
RunContext[SupportDeps]as the first argument to reach your injected dependencies, and write a real docstring, because that text is what the model reads to decide when to call it:from pydantic_ai import RunContext @agent.tool async def customer_balance( ctx: RunContext[SupportDeps], include_pending: bool ) -> float: """Return the customer's current account balance.""" return await ctx.deps.db.balance( id=ctx.deps.customer_id, pending=include_pending, )For helpers that need no context, use
@agent.tool_plaininstead. -
Run the agent with deps and read the typed result. Pass an instance of your dependencies to the run call:
deps = SupportDeps(customer_id=123, db=DatabaseConn()) result = agent.run_sync('What is my balance, including pending?', deps=deps) print(result.output) # SupportOutput(advice='Your balance is ...', block_card=False, risk=1) print(result.output.risk) # a real int, already validated: 1
That is the complete task. result.output is a fully validated SupportOutput instance, not a string you have to parse and pray over.
How Do You Force the Model to Return Valid Output?
The output_type handles structural validation for free: wrong types, out-of-range ints, and missing fields all trigger an automatic retry where the framework hands the validation error back to the model. For logic that Pydantic can't express, such as "this SQL must actually run," add an output validator and raise ModelRetry:
from pydantic_ai import ModelRetry
@agent.output_validator
async def validate_query(ctx: RunContext[SupportDeps], output: SupportOutput) -> SupportOutput:
if output.risk > 8 and not output.block_card:
raise ModelRetry("High-risk queries must block the card. Reconsider.")
return output
ModelRetry is the key tool here. Raise it from a tool or an output validator with a clear message, and the model gets that message and another attempt. The output retry budget defaults to one; raise it with Agent(..., retries=3) when a task genuinely needs more room to self-correct.
How Does Dependency Injection Keep Agents Testable?
Because every external resource arrives through deps, your agent carries no hidden global state. In production you pass a real DatabaseConn; in a unit test you pass a fake that returns fixed values, and you assert on the typed output without a network call. This is the same reason the pattern pairs well with evals: swap the deps, replay a fixture, check the structured result. If you already lean on structured outputs to get reliable JSON from an LLM, Pydantic AI is that idea promoted to a first-class agent contract rather than a hand-rolled parse step.
When Should You Pick Pydantic AI Over LangGraph or LangChain?
Pydantic AI competes with a few well-known options. They solve overlapping problems with different centers of gravity, so match the tool to the job rather than to the hype.
| Capability | Pydantic AI | LangGraph | LangChain | Raw provider SDK |
|---|---|---|---|---|
| Validated typed output | Built in via output_type |
Manual, you validate | Manual or add-on parsers | Manual JSON parsing |
| Type-safe dependency injection | Built in via deps_type |
Via graph state | Limited | None |
| Multi-step or branching control | Tools plus graphs | Its core strength | Chains and agents | You write the loop |
| Boilerplate to first result | Low | Medium | Medium to high | Low but grows fast |
| Best fit | Typed, testable single agents and tools | Complex stateful workflows | Broad integrations, RAG glue | Full control, minimal deps |
Reach for Pydantic AI when correctness and testability of a focused agent matter most. When the job is an elaborate branching state machine, read how to build a stateful AI agent with LangGraph and consider that instead; the two also compose.
What Breaks First, and How Do You Fix It?
Troubleshooting Common Errors
ImportErroror "provider not installed": you used the slim package without the right extra. Install it, for examplepip install "pydantic-ai-slim[anthropic]".- Repeated validation failures on output: your field descriptions are too vague for the model. Tighten each
Field(description=...), add constraints likegeandle, and raiseretriesif the task legitimately needs more attempts. - The model never calls your tool: the tool schema comes from the function signature and docstring. Rename cryptic parameters, add type hints, and write a docstring that states plainly when the tool applies.
API key not found: confirm the environment variable name matches the provider (OPENAI_API_KEY,ANTHROPIC_API_KEY,GEMINI_API_KEY) and that it is exported in the same shell that runs the script.- "coroutine was never awaited": you mixed sync and async. Use
agent.run_sync(...)in synchronous code, andawait agent.run(...)inside an async function. - Side effects firing twice while streaming: output validators run on partial outputs too. Check
ctx.partial_outputand skip irreversible work until the output is final.
What Should You Do Next?
- Add tracing so you can see every model call, tool invocation, and retry. Pydantic AI emits OpenTelemetry spans out of the box, which line up with the OpenTelemetry GenAI conventions for agent observability and drop straight into a Langfuse tracing setup.
- Stream results with
agent.run_stream()when you need tokens to appear as they generate rather than after the full response. - Point the agent at an OpenAI-compatible local endpoint, then reuse the tool-calling patterns for a local LLM in Ollama.
- Read the primary docs below, especially the output and models pages, before you push an agent to production.