PythonLow abstraction — you write the loop
PydanticAI
Type-safe agents where dependencies, tool arguments, and results are all Pydantic-validated, with the framework staying close to the underlying SDKs.
Architecture
- Agent[Deps, Output] is generic over a dependency type and an output type; the type checker sees both.
- Dependency injection:
deps(DB pool, HTTP client, user id) are passed torun()and reach tools and dynamic system prompts throughRunContext[Deps]. - Tools via
@agent.tool; argument schemas and validation come from the function signature; validation errors are returned to the model as retry prompts (ModelRetry). - Output types: a Pydantic model or union enforces the final answer shape;
output_validatorfunctions can reject and ask the model to retry. - Model-agnostic with per-provider
Modelclasses,FallbackModel, and first-class Pydantic Logfire / OpenTelemetry instrumentation; apydantic_graphmodule exists for explicit state machines.
Best use cases
- Python teams that already use Pydantic and FastAPI and want agents that feel like ordinary typed code.
- Tool-heavy single agents where argument validation and retries matter (Argument Validation).
- Testable systems:
TestModelandFunctionModellet you unit-test agent logic without network calls.
Weaknesses
- Fewer batteries: no built-in RAG, document loaders, or memory modules — you bring your own, which is either a feature or a gap.
- Multi-agent support is delegation via tools; there is no supervisor/handoff runtime, and graph support is lower-level than LangGraph.
- Retry-on-validation loops can quietly burn tokens if a tool schema is ambiguous; set
retriesdeliberately. - Still pre-2.0-style churn: result/output naming and streaming APIs have been renamed across releases.
- Python only; no TypeScript story for full-stack teams.
When NOT to use it
- You need durable checkpointed workflows with human interrupts out of the box.
- Your stack is TypeScript.
- You want a large integration catalogue (loaders, vector stores) rather than a runtime.
Code example
Illustrative — APIs change between versions.
1from dataclasses import dataclass2from pydantic import BaseModel, Field3from pydantic_ai import Agent, RunContext, ModelRetry4 5@dataclass6class Deps:7 db: Database8 user_id: str9 10class Recommendation(BaseModel):11 product_id: str12 reason: str = Field(max_length=200)13 14agent = Agent("anthropic:claude-sonnet-4-5", deps_type=Deps, output_type=Recommendation, # model id is version-sensitive15 system_prompt="Recommend one product the user has not bought.", retries=2)16 17@agent.tool18async def purchase_history(ctx: RunContext[Deps], limit: int = 20) -> list[str]:19 """Product ids the current user already bought."""20 return await ctx.deps.db.purchases(ctx.deps.user_id, limit)21 22@agent.output_validator23async def not_already_bought(ctx: RunContext[Deps], out: Recommendation) -> Recommendation:24 if out.product_id in await ctx.deps.db.purchases(ctx.deps.user_id, 1000):25 raise ModelRetry("That product was already purchased; pick another.")26 return out27 28result = await agent.run("Something for winter hiking", deps=Deps(db, "u-42"))29print(result.output) # validated Recommendation