Building Production-Grade AI Agents with Pydantic AI: A Practical Python Guide

AI tutorial - IT technology blog
AI tutorial - IT technology blog

The Reliability Gap: Why Deterministic Data Matters in AI

Working with Large Language Models (LLMs) often feels like gambling on a JSON response. You craft a perfect prompt, but the model occasionally returns a trailing comma, a conversational preamble, or a string where your database expects an integer. In my experience building automated support pipelines, these small inconsistencies cause over 80% of production failures in LLM-based applications.

While LangChain provides a vast ecosystem, many developers find its “string-heavy” nature difficult to debug. This is where Pydantic AI shifts the paradigm. Created by the team behind the industry-standard Pydantic validation library, this framework treats LLM outputs as structured data that must pass a schema check before your logic ever executes. It turns the “black box” of an LLM into a predictable, typed component.

Mastering type-safe agents is the difference between a prototype and a resilient system. By using Python type hints to define agent outputs, you regain the benefits of IDE autocompletion and static analysis. Instead of discovering a malformed response at 3 AM during a traffic spike, you catch schema mismatches during development.

Why Pydantic AI is gaining traction

  • Model Flexibility: Switch between OpenAI, Gemini, and Groq by changing a single line of code.
  • Automatic Validation: It leverages Pydantic v2 to ensure outputs match your exact specifications.
  • Clean Dependency Injection: Pass database connections or authenticated clients into tools without global variables.
  • Standard Python Logic: No more getting lost in complex “chains” or hidden internal graphs. It’s just Python.

Setup: Preparing Your Development Environment

Pydantic AI requires Python 3.9 or newer. I recommend using a dedicated virtual environment to avoid dependency conflicts with older libraries.

# Create and activate your environment
python -m venv venv
source venv/bin/activate  # Windows users: venv\Scripts\activate

# Install the core library and OpenAI support
pip install pydantic-ai openai

You will need an API key to proceed. While this guide uses OpenAI’s GPT-4o, the logic remains identical if you prefer Claude or local models via Ollama. Set your key in the terminal:

export OPENAI_API_KEY='your-api-key-here'

Implementation: Creating a Type-Safe Classifier

Let’s build a Support Ticket Classifier. This agent transforms messy customer emails into structured objects. If the LLM attempts to invent a category outside of our allowed list, Pydantic AI will automatically trigger a correction loop.

1. Define the Data Schema

We start by defining the “shape” of our data. Using a Pydantic model ensures every field is validated.

from pydantic import BaseModel, Field
from enum import Enum

class Category(str, Enum):
    technical = "technical"
    billing = "billing"
    feature_request = "feature_request"

class TicketClassification(BaseModel):
    category: Category
    priority: int = Field(ge=1, le=5, description="1 is low, 5 is high")
    summary: str
    is_urgent: bool

2. Initialize the Agent

Next, we configure the agent. We explicitly tell it to return a TicketClassification object rather than a raw string.

from pydantic_ai import Agent
from pydantic_ai.models.openai import OpenAIModel

model = OpenAIModel('gpt-4o')

classifier_agent = Agent(
    model,
    result_type=TicketClassification,
    system_prompt="Analyze customer tickets. Be precise with priority levels.",
)

3. Enhancing with Tools

Agents become truly useful when they interact with your existing infrastructure. Pydantic AI uses the @agent.tool decorator to grant the LLM access to external functions, such as checking a user’s subscription tier in a database.

from dataclasses import dataclass

@dataclass
class UserDeps:
    user_id: str
    has_premium: bool

@classifier_agent.tool
def check_subscription(ctx, user_id: str) -> str:
    # This mimics a database lookup
    status = "premium" if ctx.deps.has_premium else "free"
    return f"User {user_id} is on a {status} plan."

4. Executing the Workflow

Run the agent within an asynchronous function. The result.data attribute will be a fully-typed Python object, not a dictionary or a string.

async def main():
    deps = UserDeps(user_id="user_99", has_premium=True)
    
    result = await classifier_agent.run(
        "I can't access the billing portal and I need to pay my invoice immediately!",
        deps=deps
    )
    
    print(f"Category: {result.data.category}") # billing
    print(f"Priority: {result.data.priority}") # 5

if __name__ == "__main__":
    import asyncio
    asyncio.run(main())

Observability: Monitoring and Testing

Deploying an agent is only half the battle. You need to know how it behaves when things go wrong.

Self-Healing Retries

What happens if the LLM assigns a priority of “10” despite our le=5 constraint? Pydantic AI handles this gracefully. It sends the validation error back to the LLM, explains the mistake, and asks for a corrected response. This internal retry loop significantly reduces manual error-handling code.

Native Instrumentation with Logfire

Pydantic AI integrates directly with Logfire. This provides a visual timeline of every LLM call, tool execution, and validation attempt. Instead of digging through raw text logs, you get a clean dashboard showing exactly where a prompt might be failing.

import logfire

logfire.configure()
logfire.instrument_openai()
# Agent traces are now sent to the Logfire dashboard

Mocking for Unit Tests

Never test your application logic by making live API calls; it’s slow and expensive. Use the TestModel to simulate various LLM responses. This allows you to verify that your tools and dependencies work correctly in a controlled CI/CD environment without spending a cent on tokens.

from pydantic_ai.models.test import TestModel

def test_agent_logic():
    test_model = TestModel()
    with classifier_agent.override(model=test_model):
        # Run tests against the mock model
        pass

By adopting these patterns, you move away from “prompt and pray” development. You treat the AI as a structured, typed component of your software architecture—just like a database or a REST API. This discipline is what separates experimental scripts from production-ready AI applications.

Share: