Pydantic AI
An agent framework from the Pydantic team. The pitch: an agent declares a typed output and typed dependencies, so structured results and dependency injection are first-class rather than bolted on with a parser.
If you already know FastAPI, the design will feel familiar — it is the same idea (types as the contract, DI at the boundary) applied to LLM calls.
Note: as of 2026-08 this is version 2.27, and 1.0 shipped in September 2025 with an API stability commitment. Pre-1.0 tutorials use
result_type=andresult.data; both are gone. Quoting them dates you precisely.
The shape of it
from pydantic_ai import Agent
agent = Agent(
"anthropic:claude-sonnet-4-6",
instructions="Be concise, reply with one sentence.",
)
question = 'Where does "hello world" come from?'
result = agent.run_sync(question)
print(result.output)Three things to notice, because each is a deliberate design choice:
- The model is a string —
provider:model. Swapping providers is a one-line change, and the string is the whole abstraction. instructions, notsystem_prompt. Instructions are re-sent each run; they are not message history.- The answer is on
result.output, not the result object itself.
Typed output is the point
from pydantic import BaseModel
class Ticket(BaseModel):
summary: str
severity: int
needs_human: bool
agent = Agent(
"anthropic:claude-sonnet-4-6",
output_type=Ticket,
)
ticket = agent.run_sync("Printer on fire").output
ticket.severity + 1 # an int, and mypy knows itoutput_type is what separates this from calling an SDK directly. The library
turns the model into a schema, asks for structured output, validates the
response, and retries with the validation error when it does not conform.
Downstream code receives a real type instead of a string to parse.
Dependency injection
The feature people underrate. A typed dependencies object is passed into the
run and reaches tools through RunContext.
from dataclasses import dataclass
from pydantic_ai import Agent, RunContext
@dataclass
class Deps:
db: Database
agent = Agent(
"anthropic:claude-sonnet-4-6",
deps_type=Deps,
)
@agent.tool
async def balance(
ctx: RunContext[Deps], user: int
) -> float:
"""Current account balance."""
return await ctx.deps.db.balance(user)
deps = Deps(db=db)
result = await agent.run("What do I owe?", deps=deps)Note deps_type=Deps takes the type, not an instance; the instance goes to
run(). That split is what makes the whole agent typed at both ends.
Two payoffs worth naming in an interview:
- No globals. A database session or HTTP client reaches tools explicitly.
- Tests inject fakes at the same seam — pass
Deps(db=FakeDatabase())and the agent is testable without patching anything.
Tools come from signatures
The docstring becomes the tool description and the annotations become the schema. There is no separate registration object — the function is the declaration, which is why the tool and its types cannot drift apart.
Where it sits against the alternatives
| Pydantic AI | LangGraph | |
|---|---|---|
| Core idea | typed agent | state graph |
| State | in the run | checkpointed |
| Resume | no | yes, durable |
| Footprint | small | large |
| Best for | single agent | topologies |
Choose Pydantic AI for type safety, a small dependency footprint and mostly single-agent behaviour. Choose LangGraph when you need durable checkpointed state, human-in-the-loop interrupts, or a non-trivial multi-agent topology.
They are not exclusive — a Pydantic AI agent can be a node in a LangGraph graph, and that is a reasonable answer to “which would you pick”.
Versus calling the SDK directly
For one prompt and one string back, the SDK is fine and the dependency is not worth it. The framework earns its place at the point where you want validated structured output, tools with real types, and retries on schema failure — because that is roughly 200 lines of fiddly code you would otherwise write and maintain yourself.
What it does not solve
Be ready for this, because “what are its limits” follows every framework question:
- The model still has to comply. Validation catches a bad response; it does not prevent one. Budget for retries and decide what happens when they are exhausted.
- Retries cost tokens and latency. A strict schema against a weak model is an expensive loop.
- No durable state. A crash mid-run loses the run. That is LangGraph’s territory.
- Structured output narrows reasoning. Forcing a schema too early can make answers worse; let the model think, then extract.
Related
Interview angle 6
- “What is Pydantic AI and why choose it?” - a type-safe agent framework from the Pydantic team: agents declare a typed output and typed dependencies, so structured output and DI are first-class rather than bolted on. It’s a much smaller surface than LangGraph.
- “Pydantic AI or LangGraph?” - Pydantic AI when you want type safety, a small dependency footprint and mostly single-agent behaviour. LangGraph when you need durable checkpointed state, human-in-the-loop interrupts, or non-trivial multi-agent topology. They compose - an agent can be a graph node.
- “What does the typed output buy you?” - the agent’s output is a validated Pydantic model, so downstream code gets a real type instead of a string to parse, mypy checks the boundary, and a schema violation is retried with the validation error rather than silently passed on.
- “How does dependency injection work there?” - a typed dependencies object is passed into the run and reaches tools through
RunContext, so a database session or HTTP client gets to tools without globals - and tests inject fakes at the same seam. Notedeps_typetakes the type; the instance goes torun(). - “When would you skip the framework?” - one prompt, one string back. The framework earns its place when you want validated structured output, typed tools and retry-on-schema-failure, which is the code you would otherwise hand-write.
- “What are its limits?” - validation catches a bad response but does not prevent one, so retries cost tokens and latency; there is no durable state across a crash; and forcing a schema too early can narrow the model’s reasoning.