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 to run() and reach tools and dynamic system prompts through RunContext[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_validator functions can reject and ask the model to retry.
  • Model-agnostic with per-provider Model classes, FallbackModel, and first-class Pydantic Logfire / OpenTelemetry instrumentation; a pydantic_graph module 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: TestModel and FunctionModel let 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 retries deliberately.
  • 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 dataclass
2from pydantic import BaseModel, Field
3from pydantic_ai import Agent, RunContext, ModelRetry
4
5@dataclass
6class Deps:
7 db: Database
8 user_id: str
9
10class Recommendation(BaseModel):
11 product_id: str
12 reason: str = Field(max_length=200)
13
14agent = Agent("anthropic:claude-sonnet-4-5", deps_type=Deps, output_type=Recommendation, # model id is version-sensitive
15 system_prompt="Recommend one product the user has not bought.", retries=2)
16
17@agent.tool
18async 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_validator
23async 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 out
27
28result = await agent.run("Something for winter hiking", deps=Deps(db, "u-42"))
29print(result.output) # validated Recommendation

Alternatives

Related lessons