Anthropic-aligned agent framework built by the Pydantic team, using type-safe Pydantic models for agent state, tool definitions, and structured outputs.
Most agent frameworks are loosely typed: state is a dict, tools take strings, outputs are strings. This means errors surface at runtime — often in production — rather than at development time.
PydanticAI wraps agents in Python's type system. Tools are typed functions, state is a Pydantic model, and outputs are validated Pydantic objects. A type error in a tool definition is caught by your IDE or type checker before the code even runs. This is the same philosophy that made Pydantic the foundation of FastAPI and the most-downloaded Python library.
pip install pydantic-ai
from pydantic_ai import Agent
from pydantic_ai.models.anthropic import AnthropicModel
# Basic agent with a result type
agent = Agent(
model=AnthropicModel("claude-3-5-sonnet-20241022"),
system_prompt="You are a helpful assistant. Be concise.",
result_type=str # or a Pydantic model for structured output
)
# Sync run
result = agent.run_sync("What is the capital of France?")
print(result.data) # "Paris"
print(result.usage()) # token usage
# Async run
import asyncio
async def main():
result = await agent.run("What is the capital of Japan?")
print(result.data) # "Tokyo"
asyncio.run(main())
from pydantic_ai import Agent, RunContext
from pydantic_ai.models.anthropic import AnthropicModel
from pydantic import BaseModel
class WeatherData(BaseModel):
temperature_celsius: float
conditions: str
location: str
agent = Agent(
model=AnthropicModel("claude-3-5-sonnet-20241022"),
system_prompt="You help users with weather queries.",
result_type=str
)
@agent.tool
async def get_weather(ctx: RunContext[None], location: str) -> WeatherData:
'''Get current weather for a city. Returns temperature and conditions.'''
# Replace with real weather API
return WeatherData(
temperature_celsius=18.5,
conditions="Partly cloudy",
location=location
)
@agent.tool
async def get_forecast(ctx: RunContext[None], location: str, days: int) -> list[WeatherData]:
'''Get weather forecast for the next N days (max 7).'''
# Real implementation here
return [WeatherData(temperature_celsius=18.0 + i, conditions="Sunny", location=location)
for i in range(min(days, 7))]
result = agent.run_sync("What's the weather in London and will it be warmer tomorrow?")
print(result.data)
from pydantic_ai import Agent
from pydantic_ai.models.anthropic import AnthropicModel
from pydantic import BaseModel, Field
from typing import Literal
class MovieReview(BaseModel):
title: str
year: int
rating: float = Field(ge=0, le=10, description="Rating out of 10")
genre: Literal["action", "comedy", "drama", "sci-fi", "horror", "thriller"]
summary: str = Field(max_length=200)
recommended: bool
review_agent = Agent(
model=AnthropicModel("claude-3-5-haiku-20241022"),
system_prompt="You are a film critic. Return structured movie reviews.",
result_type=MovieReview # guaranteed to return a valid MovieReview
)
result = review_agent.run_sync("Review the movie Inception (2010)")
review: MovieReview = result.data
print(f"Title: {review.title}")
print(f"Rating: {review.rating}/10")
print(f"Genre: {review.genre}")
print(f"Recommended: {review.recommended}")
# All fields are type-checked — review.rating is always a float between 0 and 10
from pydantic_ai import Agent, RunContext
from pydantic_ai.models.anthropic import AnthropicModel
from dataclasses import dataclass
import httpx
@dataclass
class AgentDeps:
'''Dependencies injected into the agent at runtime.'''
http_client: httpx.AsyncClient
database_url: str
user_id: str
agent = Agent(
model=AnthropicModel("claude-3-5-sonnet-20241022"),
system_prompt="You are a personalised assistant.",
deps_type=AgentDeps, # type annotation for deps
result_type=str
)
@agent.tool
async def get_user_preferences(ctx: RunContext[AgentDeps]) -> dict:
'''Fetch the current user's preferences from the database.'''
# ctx.deps.http_client, ctx.deps.user_id are all available
# Replace with actual DB query
return {"preferred_language": "Python", "experience_level": "senior"}
@agent.system_prompt
async def personalised_prompt(ctx: RunContext[AgentDeps]) -> str:
return f"You are helping user {ctx.deps.user_id}. Be personalised."
async def main():
async with httpx.AsyncClient() as client:
deps = AgentDeps(http_client=client, database_url="...", user_id="user-42")
result = await agent.run("What language should I learn next?", deps=deps)
print(result.data)
from pydantic_ai import Agent
from pydantic_ai.models.test import TestModel # deterministic test model
# Use TestModel in tests — no API calls, deterministic, fast
agent = Agent(
model=TestModel(custom_result_text="Paris"), # always returns this
system_prompt="You answer geography questions.",
result_type=str
)
result = agent.run_sync("What is the capital of France?")
assert result.data == "Paris"
# Test tool calling
from pydantic_ai.models.test import TestModel
model = TestModel(
custom_result_text=None,
tool_calls=[("get_weather", '{"location": "London"}')] # force a tool call
)
agent_with_tools = Agent(model=model, ...)
# Verify tool is called with correct arguments
result = agent_with_tools.run_sync("Weather in London?")
PydanticAI's TestModel is designed for unit testing: no network calls, instant results, full control over what the "model" returns. Write tests before integrating with real APIs.
PydanticAI is opinionated about async. Tools decorated with @agent.tool should be async. Sync tools work but block the event loop. Wrap sync operations with asyncio.to_thread() for CPU-bound work.
deps_type must match at call time. If you define deps_type=AgentDeps but call agent.run_sync("...") without passing deps, you get a runtime error. Always pass deps when the agent's tools or system prompts depend on them.
result_type validation happens after generation. If the model produces output that can't be parsed into your result_type Pydantic model, PydanticAI retries with the validation error in the prompt. This is similar to Instructor's retry behaviour — set a max_retries limit to avoid runaway costs.
PydanticAI is a Python agent framework that uses Pydantic models as the foundation for structured LLM interactions, enabling type-safe tool definitions, validated agent outputs, and dependency injection. Built by the Pydantic team, it brings the same philosophy of explicit schemas and runtime validation to LLM agent development.
| Component | Purpose | Validation | Example |
|---|---|---|---|
| Agent | LLM orchestration | Output model | Agent(model, result_type=MyModel) |
| Tool | Function the LLM calls | Args schema auto-generated | @agent.tool def search(q: str) |
| RunContext | Dependency injection | Type-checked at runtime | ctx.deps.database.query() |
| Result type | Structured output | Pydantic model validation | class Answer(BaseModel) |
PydanticAI's tool definition uses standard Python function signatures with type annotations to automatically generate the JSON schema the LLM uses when calling the tool. A function annotated with @agent.tool that accepts str, int, and Optional[list[str]] parameters automatically produces the correct tool definition without any manual schema writing. This reduces the friction of adding new tools and eliminates schema-definition bugs where the documented schema differs from the actual function signature.
The dependency injection system in PydanticAI allows agents to access external resources — database connections, HTTP clients, configuration — through a typed dependency container rather than global state. Dependencies are declared as a type parameter on the Agent and RunContext, making the agent's requirements explicit and testable. Tests can inject mock dependencies in place of real services, enabling deterministic unit testing of agent behavior without requiring live database connections or LLM API calls.
PydanticAI's result validators add a layer of semantic validation beyond structural schema enforcement. A result validator is a Python function decorated with @agent.result_validator that receives the structured output and can raise a ModelRetry exception to request a new attempt if the output is structurally valid but semantically incorrect — for instance, if a required field is empty, if a numeric value is outside a reasonable range, or if a referenced entity cannot be found in a database. The retry mechanism feeds the validation error message back to the LLM as additional context, enabling the model to self-correct without application code handling the retry logic manually.
PydanticAI's streaming support allows results to be yielded incrementally as they are generated, enabling responsive UIs that display partial structured outputs rather than waiting for the complete result. For result types that include a text field alongside structured data, streaming the text portion while waiting for the complete structured output provides good perceived performance — the user sees the narrative response appearing in real time while the metadata is finalized. The streaming API is type-safe, yielding typed partial objects rather than raw strings.
Multi-agent architectures in PydanticAI use agent delegation where one agent calls another as a tool. The delegate agent receives a prompt from the orchestrator, executes with its own system prompt, tools, and model configuration, and returns a typed result. This composition pattern enables building complex agent systems from independently testable single-purpose agents, where each agent's behavior can be validated in isolation before integrating into the full pipeline. The typed interface between agents makes dependency contracts explicit and catches interface mismatches at development time rather than runtime.
PydanticAI's tool retry mechanism allows tools to signal recoverable failures that should be retried with modified parameters rather than propagating as exceptions. When a tool raises a ModelRetry exception with an error message, the agent feeds the message back to the LLM as context for the next tool call attempt, enabling the model to adjust its parameters — for instance, retrying a database query with corrected field names after receiving a schema validation error. This retry loop enables self-correcting agent behavior for transient tool failures without requiring explicit retry logic in the application code.