PydanticAI Structured Output Template

Agent

Template for building type-safe agents with PydanticAI structured outputs.

PydanticAI Structured Output Template

Overview

A starter template for building type-safe AI agents with PydanticAI's structured output capabilities. This template ensures your agents produce reliable, validated outputs.

Template Structure

my-pydanticai-agent/
├── agents/
│   ├── __init__.py
│   └── extractor.py
├── schemas/
│   ├── __init__.py
│   └── output.py
├── tools/
│   ├── __init__.py
│   └── data_fetcher.py
├── main.py
├── config.py
└── requirements.txt

Quick Start

1. Setup

# Create virtual environment
python -m venv venv
source venv/bin/activate

# Install dependencies
pip install pydantic-ai pydantic-ai[openai]

2. Configure API Key

export OPENAI_API_KEY="sk-..."
# or
export ANTHROPIC_API_KEY="sk-ant-..."

3. Define Output Schemas

schemas/output.py

from pydantic import BaseModel, Field
from typing import list, Optional
from enum import Enum

class Sentiment(str, Enum):
    positive = "positive"
    negative = "negative"
    neutral = "neutral"

class ArticleSummary(BaseModel):
    title: str = Field(description="The article title")
    url: str = Field(description="The article URL")
    summary: str = Field(description="A concise summary of the article")
    sentiment: Sentiment = Field(description="Overall sentiment of the article")
    key_points: list[str] = Field(description="5 key points from the article")
    confidence: float = Field(
        description="Confidence score (0-1) in the analysis",
        ge=0,
        le=1
    )

class ExtractedEntities(BaseModel):
    people: list[str] = Field(description="Names of people mentioned")
    organizations: list[str] = Field(description="Organizations mentioned")
    locations: list[str] = Field(description="Locations mentioned")
    dates: list[str] = Field(description="Important dates mentioned")

4. Create the Agent

agents/extractor.py

from pydantic_ai import Agent, RunContext
from schemas.output import ArticleSummary, ExtractedEntities
from tools.data_fetcher import fetch_article

class ExtractionAgent:
    def __init__(self, model: str = "openai:gpt-4o"):
        self.agent = Agent(
            model,
            result_type=ArticleSummary,
            system_prompt="""You are an expert content analyst.
            Extract key information from articles with high accuracy.
            Always provide confidence scores."""
        )
        
        self.agent.tool(fetch_article)
    
    def analyze(self, url: str) -> ArticleSummary:
        result = self.agent.run_sync(f"Analyze this article: {url}")
        return result.data

5. Define Tools

tools/data_fetcher.py

import httpx
from pydantic_ai import RunContext

async def fetch_article(url: str) -> str:
    """Fetch the full content of an article from a URL."""
    async with httpx.AsyncClient() as client:
        response = await client.get(url, timeout=30)
        response.raise_for_status()
        return response.text[:10000]  # Limit for context

6. Main Application

main.py

from agents.extractor import ExtractionAgent
from schemas.output import ArticleSummary

def main():
    agent = ExtractionAgent()
    
    url = "https://example.com/article"
    summary: ArticleSummary = agent.analyze(url)
    
    print(f"Title: {summary.title}")
    print(f"Sentiment: {summary.sentiment}")
    print(f"Confidence: {summary.confidence:.2%}")
    print("\nKey Points:")
    for point in summary.key_points:
        print(f"  - {point}")

if __name__ == "__main__":
    main()

Advanced Patterns

Pattern 1: Multi-Step Extraction

from pydantic_ai import Agent
from pydantic import BaseModel

class RawExtraction(BaseModel):
    text: str
    language: str

class ProcessedExtraction(BaseModel):
    summary: str
    entities: list[str]
    sentiment: str

# Step 1: Raw extraction
raw_agent = Agent('gpt-4o', result_type=RawExtraction)

# Step 2: Processed extraction
processed_agent = Agent('gpt-4o', result_type=ProcessedExtraction)

def extract_and_process(text: str) -> ProcessedExtraction:
    raw = raw_agent.run_sync(f"Extract from: {text}")
    processed = processed_agent.run_sync(
        f"Process this: {raw.data.text}",
        deps=raw.data
    )
    return processed.data

Pattern 2: Validation with Retries

from pydantic_ai import Agent, ModelRetry
from pydantic import BaseModel

class ValidatedOutput(BaseModel):
    data: dict
    quality_score: float

agent = Agent('gpt-4o', result_type=ValidatedOutput)

@agent.result_validator
def validate_output(result: ValidatedOutput) -> ValidatedOutput:
    if result.quality_score < 0.7:
        raise ModelRetry(
            f"Quality score {result.quality_score:.2f} too low, please re-analyze"
        )
    if not result.data:
        raise ModelRetry("No data extracted, please try again")
    return result

Pattern 3: Dependency Injection

from pydantic_ai import Agent, RunContext
from pydantic import BaseModel

class AppContext(BaseModel):
    api_key: str
    database_url: str
    user_preferences: dict

agent = Agent('gpt-4o', deps_type=AppContext)

@agent.tool
async def get_user_data(ctx: RunContext[AppContext], user_id: str) -> dict:
    """Fetch user data from the database."""
    # Use ctx.deps.database_url to connect
    return user_data

result = agent.run_sync(
    "Analyze user data",
    deps=AppContext(
        api_key="...",
        database_url="postgresql://...",
        user_preferences={"language": "en"}
    )
)

Pattern 4: Streaming with Validation

import asyncio
from pydantic_ai import Agent

agent = Agent('gpt-4o', result_type=ArticleSummary)

async def stream_analysis(url: str):
    async for chunk in agent.run_stream(f"Analyze: {url}"):
        # Type-safe access to partial results
        if chunk.data.summary:
            print(f"Summary so far: {chunk.data.summary}")
    
    # Final result
    result = await agent.run(f"Analyze: {url}")
    return result.data

Testing

from pydantic_ai import Agent
from pydantic_ai.test import MockModel
from schemas.output import ArticleSummary

def test_extraction():
    agent = Agent('openai:gpt-4o', result_type=ArticleSummary)
    
    # Create mock
    mock = MockModel()
    mock.add_chat_completion(
        ArticleSummary(
            title="Test Article",
            url="https://example.com",
            summary="Test summary",
            sentiment=Sentiment.positive,
            key_points=["Point 1", "Point 2"],
            confidence=0.95
        )
    )
    
    # Replace model for testing
    agent._model = mock
    
    result = agent.run_sync("Analyze this")
    assert result.data.title == "Test Article"
    assert result.data.confidence > 0.9

Best Practices

  1. Be specific with field descriptions - Helps the model understand what to extract
  2. Use enums for categorical data - Ensures valid values
  3. Add validation constraints - Use ge, le, min_length, etc.
  4. Implement result validators - Catch low-quality outputs early
  5. Use dependency injection - Makes testing easier

Troubleshooting

IssueSolution
Model ignores structureAdd more specific field descriptions
Validation fails too oftenRelax constraints or improve prompt
Tool not calledEnsure tool description is clear
Type errorsCheck Pydantic model definitions

Resources


Last updated: May 2026

View on GitHub

Related Templates