PydanticAI Structured Output Template
AgentTemplate 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
- Be specific with field descriptions - Helps the model understand what to extract
- Use enums for categorical data - Ensures valid values
- Add validation constraints - Use
ge,le,min_length, etc. - Implement result validators - Catch low-quality outputs early
- Use dependency injection - Makes testing easier
Troubleshooting
| Issue | Solution |
|---|---|
| Model ignores structure | Add more specific field descriptions |
| Validation fails too often | Relax constraints or improve prompt |
| Tool not called | Ensure tool description is clear |
| Type errors | Check Pydantic model definitions |
Resources
Last updated: May 2026
