guides

Pydantic AI: The Production Guide (Past Hello World)

Written by Mert Batur
Apr 1, 2026
10 read
Pydantic AI: The Production Guide (Past Hello World)

Pydantic AI: The Production Guide (Past Hello World)

Raw LLM outputs break apps. You ask for JSON, you get markdown. You ask for a number between 1 and 10, you get "Sure! Here's a number: seven." If you've built anything real with LLM APIs, you've written defensive parsing code that makes you question your career choices. Pydantic AI fixes this, it's the type-safe agent framework built by the same team behind Pydantic and FastAPI. Think of it as "FastAPI for AI agents": you define what you want with Python type hints, and the framework handles validation, retries, and tool calling.

This Pydantic AI guide is for developers who've already run their first LLM call and want production patterns: structured outputs that don't break, dependency injection for testable agents, and real-world tools beyond weather APIs. By the end, you'll have working agents with tools, DI, streaming, and tests.

<!-- IMAGE: Pydantic AI agent architecture, Agent receives prompt, calls tools via RunContext, validates output through Pydantic model -->

Pydantic AI at a Glance

AttributeDetails
What it isType-safe AI agent framework for Python
Built byPydantic team (Samuel Colvin et al.)
Philosophy"FastAPI for AI agents", type hints drive everything
LicenseMIT (open-source)
Current Versionv1.74.0 (March 2026)
Python Version3.9+
Supported ModelsOpenAI, Anthropic, Google Gemini, Groq, Mistral, Ollama, and more
Key FeaturesStructured outputs, tool calling, dependency injection, streaming, TestModel
GitHub Stars16,000+
Production ReadyYes, v1.0 released September 2025
ObservabilityNative Logfire integration (OpenTelemetry-based)
Learning CurveLow if you know Pydantic/FastAPI; moderate otherwise

The standout features are structured outputs (validated with Pydantic models), dependency injection (like FastAPI's Depends), and TestModel (mock LLM for testing without API calls). If you're coming from LangChain and wondering "is there something cleaner?", this is probably it.

Installation and First Agent

bash
# Install with OpenAI support (swap openai for anthropic, google, etc.)
pip install "pydantic-ai[openai]"

# Set your API key
export OPENAI_API_KEY="sk-..."

Your first agent in 5 lines:

python
from pydantic_ai import Agent

agent = Agent("openai:gpt-4o", system_prompt="You are a helpful assistant.")

result = agent.run_sync("What's the capital of France?")
print(result.output)  # "Paris"

That's it. Agent wraps the model, run_sync sends a prompt and returns a result. The result.output is a plain string here, but that's about to change.

Structured Outputs, Why Pydantic AI Exists

This is the core feature. Instead of getting a string back from the LLM and hoping it's valid JSON, you define a Pydantic model and the agent returns a validated Python object.

Before: Raw LLM Output

python
# The old way -- hope for the best
from openai import OpenAI

client = OpenAI()
response = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "Review the movie Inception. Return JSON with title, rating (1-10), summary."}]
)
# response.choices[0].message.content is a string
# Maybe it's JSON. Maybe it has markdown code fences. Maybe rating is "eight".
# You're on your own.

After: Structured with Pydantic AI

python
from pydantic import BaseModel
from pydantic_ai import Agent

class MovieReview(BaseModel):
    title: str
    rating: int  # Guaranteed to be an int, not "eight"
    summary: str
    recommended: bool

agent = Agent("openai:gpt-4o", result_type=MovieReview)

result = agent.run_sync("Review the movie Inception")
review = result.output  # This is a MovieReview instance, not a string

print(f"{review.title}: {review.rating}/10")
print(f"Recommended: {review.recommended}")
print(review.summary)

The difference is night and day. result.output is a real MovieReview object. If the LLM returns rating: "eight" instead of rating: 8, Pydantic's validation catches it. For a deeper look at how this works across different providers, see our guide on structured outputs across LLM providers.

What Happens When Validation Fails

Here's the part no other tutorial shows: what happens when the LLM messes up?

python
from pydantic import BaseModel, Field
from pydantic_ai import Agent

class StrictReview(BaseModel):
    title: str
    rating: int = Field(ge=1, le=10)  # Must be 1-10
    pros: list[str] = Field(min_length=2)  # At least 2 pros

agent = Agent("openai:gpt-4o", result_type=StrictReview)

# If the LLM returns rating=15 or only 1 pro:
# 1. Pydantic validation fails
# 2. The error message is sent BACK to the LLM
# 3. The LLM tries again with the corrected output
# 4. This repeats up to the retry limit
result = agent.run_sync("Review the movie Inception")

This retry-with-feedback loop is Pydantic AI's killer feature. The LLM learns from its own validation errors. You don't write retry logic, the framework handles it.

Verdict: Structured outputs are the single best reason to use Pydantic AI over raw API calls. If you're parsing LLM JSON by hand, stop.

Tools and Function Calling

Tools let your agent call Python functions to get real data. Instead of the LLM hallucinating facts, it can query your database, search your docs, or call an API.

Registering a Tool

python
from pydantic_ai import Agent

agent = Agent("openai:gpt-4o")

@agent.tool
async def search_docs(query: str) -> str:
    """Search the documentation for relevant articles."""
    # Your actual search logic here
    results = await doc_search_engine.search(query, limit=5)
    return "\n".join(r.title + ": " + r.snippet for r in results)

The @agent.tool decorator registers the function. Pydantic AI reads the function's type hints and docstring to tell the LLM what the tool does, what arguments it takes, and what it returns. No manual schema writing, your type hints ARE the schema. For background on how LLM function calling works under the hood, we have a dedicated guide.

RunContext: Passing Data to Tools

Here's where Pydantic AI diverges from other frameworks. RunContext lets you pass runtime data (database connections, user info, API clients) to your tools without global state.

python
from dataclasses import dataclass
from pydantic_ai import Agent, RunContext

@dataclass
class SupportDeps:
    customer_id: str
    db_connection: DatabaseConnection

agent = Agent("openai:gpt-4o", deps_type=SupportDeps)

@agent.tool
async def get_order_history(ctx: RunContext[SupportDeps], limit: int = 5) -> str:
    """Fetch recent orders for the current customer."""
    orders = await ctx.deps.db_connection.query(
        "SELECT * FROM orders WHERE customer_id = $1 ORDER BY date DESC LIMIT $2",
        ctx.deps.customer_id, limit
    )
    return format_orders(orders)

The ctx.deps gives the tool access to whatever you passed at runtime. The tool doesn't import a global database connection, it receives one. This is dependency injection, and it's what makes your agents testable.

A Real-World Tool Example

python
@agent.tool
async def run_sql_query(ctx: RunContext[SupportDeps], sql: str) -> str:
    """Run a read-only SQL query against the analytics database.
    Only SELECT queries are allowed."""
    if not sql.strip().upper().startswith("SELECT"):
        return "Error: only SELECT queries are allowed"
    results = await ctx.deps.db_connection.fetch(sql)
    return json.dumps(results, default=str)

Verdict: Tool calling in Pydantic AI is cleaner than any other framework thanks to type hints doing the heavy lifting. You write normal Python functions with type annotations. The framework figures out the rest.

Dependency Injection, The Feature LangChain Wishes It Had

If you've used FastAPI's Depends, you already understand Pydantic AI's DI system. If you haven't, here's the short version: instead of your agent reaching out to grab what it needs (global database connections, API clients, config), you hand it everything at runtime.

Defining Dependencies

python
from dataclasses import dataclass
from pydantic_ai import Agent

@dataclass
class AppDeps:
    db: AsyncDatabasePool
    search_client: SearchAPIClient
    current_user: User

agent = Agent(
    "openai:gpt-4o",
    deps_type=AppDeps,
    system_prompt="You are a customer support agent."
)

Using Dependencies in Tools

python
@agent.tool
async def lookup_account(ctx: RunContext[AppDeps]) -> str:
    """Look up the current user's account details."""
    account = await ctx.deps.db.fetchrow(
        "SELECT * FROM accounts WHERE user_id = $1",
        ctx.deps.current_user.id
    )
    return json.dumps(account, default=str)

# Run with real dependencies
result = await agent.run(
    "What's my account status?",
    deps=AppDeps(db=real_db, search_client=real_search, current_user=user)
)

Why DI Makes Your Agents Testable

This is the real payoff. In LangChain, you'd pass context through chain kwargs or closures, there's no standard pattern. In Pydantic AI, swapping real dependencies for test doubles is trivial:

python
# In your test file
from pydantic_ai import Agent
from your_app import agent, AppDeps

async def test_account_lookup():
    mock_deps = AppDeps(
        db=MockDatabase({"user_123": {"status": "active", "plan": "pro"}}),
        search_client=MockSearch(),
        current_user=User(id="user_123")
    )
    result = await agent.run("What's my account status?", deps=mock_deps)
    assert "active" in result.output
    assert "pro" in result.output

No monkey-patching. No mocking global imports. You just pass different deps.

Verdict: Dependency injection is why experienced Python developers prefer Pydantic AI. It's the FastAPI influence showing.

Model Providers, OpenAI, Anthropic, Gemini, Ollama

Pydantic AI is model-agnostic. Switching providers is a one-line change:

python
# OpenAI
agent = Agent("openai:gpt-4o")

# Anthropic
agent = Agent("anthropic:claude-sonnet-4-20250514")

# Google Gemini
agent = Agent("google-gla:gemini-2.0-flash")

# Local Ollama
agent = Agent("ollama:llama3.1")

Everything else, tools, structured outputs, DI, stays identical. Your business logic doesn't change when you switch models.

ProviderModelsFree TierSetup Complexity
OpenAIGPT-4o, GPT-4o mini, o1$5 credit (new accounts)Low, API key only
AnthropicClaude Sonnet, Haiku, OpusNo free tierLow, API key only
Google GeminiGemini 2.0 Flash, ProGenerous free tierMedium, project setup
GroqLlama, MixtralFree tier availableLow, API key only
Ollama (local)Llama, Mistral, Phi, etc.Completely freeMedium, install Ollama

Verdict: Model-agnostic design means you're never locked into one provider. Start with OpenAI for convenience, benchmark with Anthropic, and use Ollama for local development.

Streaming Responses

For chat UIs and real-time applications, streaming is essential. Pydantic AI supports it while maintaining type safety:

python
from pydantic_ai import Agent
from pydantic import BaseModel

class AnalysisResult(BaseModel):
    summary: str
    sentiment: str
    confidence: float

agent = Agent("openai:gpt-4o", result_type=AnalysisResult)

async def stream_analysis(text: str):
    async with agent.run_stream(f"Analyze this text: {text}") as stream:
        async for partial in stream.stream_structured():
            # partial is a partially-validated AnalysisResult
            print(f"Streaming: {partial}")

        # Final result is fully validated
        result = await stream.get_output()
        print(f"Final: {result.summary} ({result.confidence:.0%} confident)")

This works beautifully with FastAPI's StreamingResponse, same ecosystem, same patterns. The Pydantic AI agents docs cover advanced streaming options including text-only streaming with stream_text().

Pydantic AI vs LangGraph vs OpenAI Agents SDK

You're here, so you're probably asking: "should I use Pydantic AI or LangGraph?" Honest answer: they solve different problems, and you might use both.

Feature Comparison Table

FeaturePydantic AILangGraphOpenAI Agents SDK
Type SafetyFull (Pydantic models)Partial (TypedDict)Minimal
Dependency InjectionBuilt-in (FastAPI-style)NoneNone
Structured OutputsNative with retryVia output parsersVia JSON mode
Tool Calling@agent.tool decorator@tool decoratorfunction definitions
Multi-AgentBasic handoffsAdvanced (state machines)Handoffs + guardrails
StreamingTyped streamingStreaming eventsStreaming
Model Support10+ providersPrimarily LangChain modelsOpenAI only
TestingTestModel built-inNo built-in testingNo built-in testing
Learning CurveLow (if you know Pydantic)High (graph concepts)Low (simple API)
Community SizeGrowing (16K stars)Large (LangChain ecosystem)Growing (OpenAI backing)
Best ForClean, testable agentsComplex state workflowsOpenAI-only projects

When to Use Each

Choose Pydantic AI when you want clean, type-safe agent code. It's ideal for single-agent tasks with tools (customer support bots, data extraction, code review agents) and situations where testability matters. If your team already uses FastAPI and Pydantic, the learning curve is almost flat.

Choose LangGraph when you need complex multi-step workflows with conditional branching, human-in-the-loop approval, and sophisticated state management. LangGraph excels at orchestrating multiple steps, not individual agent quality. For a deep dive, see our full LangGraph vs CrewAI vs OpenAI Agents SDK comparison.

Choose OpenAI Agents SDK when you're 100% on OpenAI, want the simplest possible setup, and don't need multi-provider support or DI.

The Combination Pattern

Here's what experienced teams actually do: use Pydantic AI for individual agents (clean code, testable, typed outputs) and LangGraph for orchestration between agents (routing, state machines, conditional logic). They're not competing, they're complementary layers.

python
# Pydantic AI agent -- clean, testable, type-safe
support_agent = Agent("openai:gpt-4o", result_type=SupportResponse, deps_type=SupportDeps)

# LangGraph graph -- orchestrates when to call which agent
graph = StateGraph(SupportState)
graph.add_node("classify", classify_intent)
graph.add_node("support", lambda state: support_agent.run_sync(state["query"]))
graph.add_node("escalate", escalate_to_human)

Verdict: Choose Pydantic AI for clean, testable agent code. Choose LangGraph for complex multi-step workflows. They're not mutually exclusive.

Testing Your Agents with TestModel

This is the section that separates a beginner guide from a production guide. Every real codebase needs tests, and testing agents is notoriously hard, LLM calls are slow, expensive, and non-deterministic. Pydantic AI ships a solution: TestModel.

python
from pydantic_ai import Agent
from pydantic_ai.models.test import TestModel
from pydantic import BaseModel

class SupportResponse(BaseModel):
    answer: str
    confidence: float
    escalate: bool

agent = Agent("openai:gpt-4o", result_type=SupportResponse)

# In tests: swap the real model for TestModel
def test_support_agent():
    with agent.override(model=TestModel()):
        result = agent.run_sync("I need help with billing")
        # TestModel returns valid structured data matching your result_type
        assert isinstance(result.output, SupportResponse)
        assert isinstance(result.output.confidence, float)
        assert isinstance(result.output.escalate, bool)

TestModel generates valid data that matches your result_type without making any API calls. Zero cost, deterministic, fast. The Pydantic AI testing docs cover advanced patterns like FunctionModel for custom responses and capture_run_messages for inspecting tool calls.

Testing Tools and DI Together

python
def test_order_lookup_tool():
    # Mock dependencies
    mock_deps = SupportDeps(
        customer_id="test-123",
        db_connection=MockDB(orders=[{"id": "ord-1", "status": "shipped"}])
    )

    with agent.override(model=TestModel()):
        result = agent.run_sync(
            "Where is my order?",
            deps=mock_deps
        )
        assert isinstance(result.output, SupportResponse)

No API calls. No flaky tests. No cost. Run this in CI/CD alongside the rest of your test suite.

This is the #1 content gap on the entire SERP. No other Pydantic AI guide covers testing. If you're building agents for production, this is what you need.

Observability, Logfire Integration in 5 Minutes

Production agents need AI observability. You want to see every LLM call, tool invocation, latency, token count, and cost. Pydantic AI integrates natively with Logfire, the Pydantic team's observability platform (built on OpenTelemetry).

python
import logfire
from pydantic_ai import Agent

logfire.configure()  # Uses LOGFIRE_TOKEN env var
logfire.instrument_pydantic_ai()

agent = Agent("openai:gpt-4o", result_type=MovieReview)
# Every run is now traced automatically
result = agent.run_sync("Review Inception")

Three lines. You get full traces showing: prompt sent, model response, tool calls (if any), validation passes/failures, retries, latency, and estimated cost. If Logfire isn't your thing, Langfuse is a solid open-source alternative with context engineering support for tracing how your prompts evolve.

FAQ

What is Pydantic AI and how is it different from LangChain?

Pydantic AI is a type-safe agent framework where Python type hints drive validation, tool schemas, and dependency injection. LangChain is a larger framework focused on chaining LLM calls together. The key difference: Pydantic AI validates outputs at the framework level and provides built-in dependency injection for testability, LangChain does neither by default.

How do I build a type-safe AI agent with Pydantic AI?

Define a Pydantic BaseModel for your output, pass it as result_type to Agent, and call run_sync() or run(). The agent returns a validated instance of your model, not a raw string. See the Structured Outputs section for complete examples.

Should I use Pydantic AI or LangGraph for production agents?

Use Pydantic AI for individual agents where type safety, testability, and clean code matter. Use LangGraph for orchestrating complex multi-step workflows with conditional routing. Many teams use both, Pydantic AI agents inside a LangGraph orchestration layer.

How does Pydantic AI handle tool calling and dependency injection?

Decorate a function with @agent.tool and Pydantic AI reads its type hints to generate the tool schema. For DI, set deps_type on the Agent and accept RunContext[YourDeps] in tools. Runtime dependencies (DB connections, API clients) flow through without global state.

How do I add streaming to a Pydantic AI agent?

Use agent.run_stream() instead of agent.run(). It returns an async context manager that yields partial results via stream_structured() or stream_text(). The final result is still fully validated against your result_type.

Is Pydantic AI production-ready in 2026?

Yes. Version 1.0 shipped in September 2025 with an API stability commitment. It's backed by the Pydantic team (the most-downloaded Python library for data validation) and currently at v1.74.0 with regular updates.

Can I use Pydantic AI with Ollama and local models?

Yes. Use Agent("ollama:llama3.1") and make sure Ollama is running locally. Install the ollama provider extra: pip install "pydantic-ai[ollama]". Structured outputs and tools work the same as with cloud providers.

How do I test Pydantic AI agents?

Use TestModel, a mock model that generates valid structured data matching your result_type without API calls. Wrap your test in agent.override(model=TestModel()) and run assertions on the output. See the Testing section for complete pytest examples.

Does Pydantic AI work with FastAPI?

Perfectly. They share the same dependency injection philosophy and are built by the same team. You can use Pydantic AI agents inside FastAPI endpoints, share dependency types between them, and stream agent responses through StreamingResponse.

What's the difference between Pydantic AI and the OpenAI Agents SDK?

Pydantic AI is model-agnostic (works with OpenAI, Anthropic, Gemini, Ollama, etc.), has dependency injection, TestModel for testing, and Pydantic validation. The OpenAI Agents SDK is simpler but locked to OpenAI models and lacks DI and built-in testing. Choose Pydantic AI for flexibility; choose OpenAI Agents SDK for the simplest possible OpenAI-only setup.

Key Takeaways and Next Steps

ConceptKey InsightNext Step
Structured OutputsYour result_type is validated and retried automaticallyDefine Pydantic models for all agent outputs
ToolsType hints ARE the schema, no manual definitionsBuild tools with @agent.tool and RunContext
Dependency InjectionPass runtime deps explicitly for testabilityDefine a deps_type dataclass for every agent
TestingTestModel eliminates API costs in CI/CDAdd agent.override(model=TestModel()) to your test suite
Model ProvidersOne-line model switching, no code changesStart with OpenAI, benchmark alternatives later
Observability3-line Logfire setup for full tracesAdd logfire.instrument_pydantic_ai() to production

Start with a small agent that has structured outputs. Add a tool. Add dependencies. Write a test with TestModel. That's the production path, and you now have everything you need to walk it.

The official Pydantic AI docs and GitHub repository are excellent for going deeper. The framework moves fast, so bookmark the changelog.

Sources

Tags

pydantic aiai agentsstructured outputsdependency injectiontype-safe agentspythonllm framework

Share this article

Start Your Project

Ready to build something extraordinary?

Let's turn your vision into reality. Our team is ready to help you create software that makes a difference.