ai-agents #pydanticai#ai-agents#python#langchain#type-safety#production

PydanticAI in Production: Why Python Engineers Are Replacing LangChain

S

S L Manikanta

Aug 29, 2026 9 min read

✉ Newsletter

Want to build production-ready AI?

Subscribe to StackMindset to receive actionable systems engineering checklists and code walkthroughs. No spam, only technical insights.

If you have built an LLM application in Python over the last three years, you have probably wrestled with framework bloat. You imported a chain, discovered it wrapped three dictionaries in four abstract classes, tried to debug a failing prompt, and found yourself tracing through twelve layers of stack frames just to see the raw HTTP request.

PydanticAI represents the inevitable backlash against that complexity.

Built by the creators of Pydantic, the library rejects proprietary expression languages, nested callbacks, and loose string parsing. Instead, it treats AI agents like ordinary Python backend code: static types, explicit dependency injection, validated outputs, and clean test harnesses.

Here is why engineering teams are adopting it for production, how it actually works, and where it fits alongside orchestration engines like LangGraph.

The Problem with Traditional Agent Frameworks

Most early AI libraries were designed during the prototype boom of 2023. They prioritized connecting dozens of external APIs in two lines of copy-pasted demo code. That worked well for hackathons, but it created severe maintenance debt in production environments:

  1. Lost Type Safety: Tool arguments and outputs were passed as untyped strings or arbitrary dictionaries. A typo in a parameter name only failed at runtime when an LLM hallucinated a key.
  2. Framework Lock-in: Custom domain-specific languages (like LCEL) forced backend teams to learn proprietary composition syntax rather than using standard Python control flow.
  3. Hidden Control Flow: When an agent entered an infinite tool loop, understanding the internal state required injecting custom hooks and reading verbose console logs.
  4. Mocking and Testing Nightmares: Testing an agent without making real API calls required mocking complex internal class hierarchies rather than swapping out a clean dependency interface.

PydanticAI was engineered specifically to eliminate these four problems.

Core Concepts: How PydanticAI Works

PydanticAI models an agent around three primary building blocks:

  • The Agent Definition: Binds a model provider (OpenAI, Anthropic, Gemini, Groq, Ollama) with a typed result schema and system instructions.
  • Tools with Schema Inference: Regular Python functions where parameter types and docstrings automatically generate the tool schema sent to the model.
  • Dependency Injection (RunContext): A mechanism to inject databases, HTTP clients, and user sessions into tools and dynamic prompts safely at runtime.

Let us look at a real, complete example.

from dataclasses import dataclass
import httpx
from pydantic import BaseModel, Field
from pydantic_ai import Agent, RunContext

# 1. Define structured output schema
class StockAnalysis(BaseModel):
    ticker: str = Field(description="The stock ticker symbol")
    current_price: float = Field(description="Current market price in USD")
    recommendation: str = Field(description="Hold, Buy, or Sell rating with rationale")
    risk_factors: list[str] = Field(description="Top 3 identified downside risks")

# 2. Define runtime dependencies
@dataclass
class MarketDeps:
    http_client: httpx.AsyncClient
    api_key: str
    max_risk_level: str = "medium"

# 3. Create the agent with typed output and dependencies
analyst_agent = Agent(
    model="anthropic:claude-3-7-sonnet-latest",
    result_type=StockAnalysis,
    deps_type=MarketDeps,
    system_prompt=(
        "You are an institutional equity research analyst. "
        "Always evaluate cash flows and verify real-time pricing before rating."
    )
)

# 4. Attach typed tools with automatic context injection
@analyst_agent.tool
async def get_live_price(ctx: RunContext[MarketDeps], ticker: str) -> float:
    """Fetch the current stock price from the market data service."""
    resp = await ctx.deps.http_client.get(
        f"https://api.marketdata.internal/v1/quote/{ticker}",
        headers={"Authorization": f"Bearer {ctx.deps.api_key}"}
    )
    resp.raise_for_status()
    return resp.json()["price"]

# 5. Run the agent
async def analyze_equity(ticker: str) -> StockAnalysis:
    async with httpx.AsyncClient() as client:
        deps = MarketDeps(http_client=client, api_key="secret-key")
        result = await analyst_agent.run(
            f"Provide a comprehensive investment analysis for {ticker}",
            deps=deps
        )
        # result.data is guaranteed to be an instance of StockAnalysis
        return result.data

Notice what is missing: no custom chain syntax, no custom output parsers, and no global state. It is standard Python async code with full IDE autocompletion and static type analysis via mypy or pyright.

Dynamic System Prompts and Context Injection

In real backend applications, system prompts are never static. They depend on who is calling the service, what permissions they have, and their account configuration.

PydanticAI lets you declare dynamic system prompts as functions that receive the injected dependencies:

@analyst_agent.system_prompt
async def add_user_risk_tolerance(ctx: RunContext[MarketDeps]) -> str:
    return f"The client has a strict risk ceiling of '{ctx.deps.max_risk_level}'. Never recommend high-volatility assets."

When agent.run() executes, PydanticAI automatically evaluates all dynamic system prompts in order, merges them with the static prompt, and delivers the combined context to the LLM.

Structured Output and Automatic Retries

One of the most fragile points in production AI is handling malformed JSON from an LLM. Standard frameworks either crash or return a raw string you have to inspect manually.

PydanticAI solves this through an automatic validation loop:

graph TD
    User["User Prompt + Dependencies"] --> Agent["PydanticAI Agent"]
    Agent --> LLM["LLM Inference"]
    LLM --> SchemaCheck{"Pydantic Schema<br/>Validation"}
    SchemaCheck -->|Valid| Success["Return Typed Object"]
    SchemaCheck -->|Invalid| Feedback["Send ValidationError back to LLM"]
    Feedback --> LLM

When the LLM outputs a payload that violates your Pydantic schema (for example, missing a required field or providing a string where a float was expected), PydanticAI catches the ValidationError, sends the exact validation error message back to the LLM as a tool correction message, and requests a corrected payload.

You can customize this retry behavior directly in your model declarations:

from pydantic import field_validator

class UserProfile(BaseModel):
    username: str
    age: int
    email: str

    @field_validator("email")
    def validate_work_email(cls, v: str) -> str:
        if "@company.com" not in v: 
            raise ValueError("Must provide an authorized @company.com email address.")
        return v

user_agent = Agent("openai:gpt-4.1", result_type=UserProfile, retries=3)

If the model tries to return [email protected], PydanticAI rejects it, passes your custom error message back into the model context, and prompts the model to correct its answer.

Testing AI Agents Without Mocking the Internet

Testing agentic workflows is notoriously difficult when tools call live APIs. Because PydanticAI separates agent logic from dependency construction, writing unit tests is straightforward.

You can swap real dependencies for in-memory mocks, and you can swap live model calls for deterministic test models:

import pytest
from pydantic_ai.models.test import TestModel

@pytest.mark.asyncio
async def test_analyst_agent_schema():
    # Use TestModel to simulate LLM responses without network calls
    test_model = TestModel(
        custom_result_args={
            "ticker": "NVDA",
            "current_price": 128.50,
            "recommendation": "Buy due to sustained data center demand",
            "risk_factors": ["Supply constraints", "Export regulations", "Customer concentration"]
        }
    )

    async with httpx.AsyncClient() as client:
        deps = MarketDeps(http_client=client, api_key="test-key")
        
        # Override the agent model during tests
        with analyst_agent.override(model=test_model):
            result = await analyst_agent.run("Analyze NVDA", deps=deps)
            
            assert result.data.ticker == "NVDA"
            assert result.data.current_price == 128.50
            assert len(result.data.risk_factors) == 3

This runs in milliseconds in your CI/CD pipeline, guaranteeing that your schemas, tool signatures, and downstream handlers remain valid on every pull request.

Production Architecture: PydanticAI with LangGraph

A common misconception is that PydanticAI and LangGraph are direct competitors. While both can build agents, they operate at different layers of abstraction:

  • PydanticAI excels at the Agent Node Level: tool execution, type safety, dependency injection, and schema extraction.
  • LangGraph excels at the System Orchestration Level: durable state machines, checkpointing, multi-day human-in-the-loop flows, and cross-team subgraphs.

In modern enterprise architectures, teams frequently combine both:

graph LR
    subgraph Orchestration [LangGraph Orchestration Layer]
        Router["Routing Node"] --> Node1["Research Node (PydanticAI)"]
        Node1 --> HumanCheck["Human Review Checkpoint"]
        HumanCheck --> Node2["Drafting Node (PydanticAI)"]
        Node2 --> Publisher["Publishing Node"]
    end

Using PydanticAI inside your LangGraph nodes gives you the best of both worlds: bulletproof type validation inside each discrete task, coordinated by a durable state machine at the top level.

Comparison: PydanticAI vs LangChain vs Instructor

DimensionPydanticAILangChainInstructor
Primary PhilosophyFull-featured type-safe agentsKitchen-sink frameworkStructured outputs patch
Tool CallingNative with RunContextAbstract BaseTool classesFunction calling wrapper
Dependency InjectionBuilt-in first-class citizenNoneNone
Learning CurveLow (standard Python)High (custom concepts)Low (wraps client)
Ecosystem SizeFast growingMassive (700+ connectors)Focused
Code MaintainabilityHighLow to MediumHigh
Best Used ForProduction agent backendsPrototyping multi-tool demosSimple JSON extraction

When to Choose PydanticAI

Choose PydanticAI if:

  • You are building backend services with FastAPI or modern Python frameworks.
  • You care about static analysis, IDE autocompletion, and clear stack traces.
  • You need structured outputs that never break downstream database schemas.
  • Your tools need access to authenticated sessions, databases, and connection pools.
  • You want tests that run in CI without mock-patching third-party SDK internals.

Avoid PydanticAI if:

  • You are looking for a no-code visual builder.
  • You require pre-built integrations for hundreds of niche SaaS tools out of the box without writing custom client wrappers.

Frequently Asked Questions

What models does PydanticAI support?

PydanticAI supports all major frontier and open-source models out of the box, including OpenAI (GPT-4.1, o3), Anthropic (Claude 3.7 Sonnet, Claude Opus 4), Google Gemini (2.5 Pro, 2.5 Flash), Groq, Mistral, and local models via Ollama and vLLM.

How does PydanticAI handle streaming?

PydanticAI supports full asynchronous token streaming and structured streaming. Using agent.run_stream(), you can stream text tokens directly to a frontend client or stream structured Pydantic object updates incrementally as fields are validated.

Can PydanticAI replace LangChain completely?

For many backend engineering applications, yes. If your primary use case is calling models, executing internal tools, and returning structured data, PydanticAI offers a significantly cleaner and more maintainable architecture. If you rely on LangChain for its extensive library of pre-built document loaders and vector store connectors, you can still use those specific utility packages alongside PydanticAI.

Is PydanticAI ready for high-throughput production?

Yes. PydanticAI was designed with zero global mutable state, making it thread-safe and fully compatible with asynchronous event loops under ASGI servers like Uvicorn and Hypercorn. It integrates directly with OpenTelemetry and Logfire for distributed tracing and latency monitoring.

✉ Newsletter

Want to build production-ready AI?

Subscribe to StackMindset to receive actionable systems engineering checklists and code walkthroughs. No spam, only technical insights.

S

Written by S L Manikanta

AI Engineer specializing in agentic workflows, multi-step LLM validation pipelines, and secure cloud environments. Sharing practical lessons from building software.

Related Articles

ai-agents
The Shift to Agentic AI Workflows in Production

Why engineering teams are moving away from simple copilots to autonomous agentic workflows, and the technical challenges of long-running state management.

ai-agents
AI Agent Memory: Short-Term vs Long-Term Memory

A complete architectural breakdown of how AI agents manage state, covering short-term conversational context and long-term persistent memory systems.

ai-agents
AI Agent Observability: Logs, Traces, and Metrics in Production

A complete technical reference and implementation guide to observing agentic workflows, tracking LLM token costs, logging reasoning trajectories, tracing nested tool calls, and monitoring system metrics in production.