PydanticAI vs OpenAI Agents SDK
Type-safe AI frameworks compared
Overview
Type-safe AI frameworks compared
Verdict
Type-safe AI frameworks compared
Details
PydanticAI vs OpenAI Agents SDK
Overview
A detailed comparison of two modern Python AI agent frameworks: PydanticAI (by Pydantic) and OpenAI Agents SDK (by OpenAI). Both provide type-safe, structured approaches to building AI agents, but with different philosophies and strengths.
Quick Comparison Table
| Feature | PydanticAI | OpenAI Agents SDK |
|---|---|---|
| Maintainer | Pydantic (Samuel Colvin) | OpenAI |
| Type Safety | ⭐⭐⭐⭐⭐ Excellent (Pydantic) | ⭐⭐⭐⭐ Very Good |
| Model Support | Multi-provider (OpenAI, Anthropic, Google, Groq, DeepSeek) | OpenAI models primarily |
| Structured Outputs | Native Pydantic integration | Native Pydantic support |
| Tool Calling | Type-safe with Pydantic models | First-class tool support |
| State Management | Built-in conversation state | Built-in state management |
| Streaming | Full streaming support | Full streaming support |
| Multi-Agent | Supported via dependencies | Supported via handoffs |
| Learning Curve | Moderate (Pydantic knowledge) | Low (OpenAI API familiarity) |
| Documentation | Excellent | Excellent |
| Community | Growing | Large (OpenAI ecosystem) |
Philosophy
PydanticAI
"Type-safe AI by default"
PydanticAI is built on the Pydantic ecosystem, emphasizing:
- Type correctness: Everything is typed, validated, and documented
- Developer experience: IDE autocomplete, type hints, validation errors
- Multi-provider: Works with any LLM provider that supports tool calling
- Explicit over implicit: Clear contracts between components
from pydantic_ai import Agent
from pydantic import BaseModel
class ResearchResult(BaseModel):
title: str
summary: str
key_findings: list[str]
confidence: float
agent = Agent(
'openai:gpt-4o',
result_type=ResearchResult, # Type-safe result
)
result = agent.run_sync('Research quantum computing advances')
# result.data is typed as ResearchResult
OpenAI Agents SDK
"AI agents, simplified"
OpenAI Agents SDK emphasizes:
- Simplicity: Minimal boilerplate, intuitive API
- OpenAI-first: Deep integration with OpenAI's ecosystem
- Production-ready: Built for real-world applications
- Streaming-first: Native streaming support throughout
from openai import OpenAI
from openai.types.beta import Assistant
client = OpenAI()
assistant = client.beta.assistants.create(
name="Research Assistant",
model="gpt-4o",
tools=[{"type": "file_search"}],
)
thread = client.beta.threads.create()
message = client.beta.threads.messages.create(
thread_id=thread.id,
role="user",
content="Research quantum computing advances",
)
run = client.beta.threads.runs.create(
thread_id=thread.id,
assistant_id=assistant.id,
)
Type Safety
PydanticAI
PydanticAI's type safety is its killer feature:
from pydantic_ai import Agent
from pydantic import BaseModel, Field
class AnalysisOutput(BaseModel):
"""Structured output for analysis tasks."""
executive_summary: str = Field(
description="Brief summary of key findings"
)
key_metrics: dict[str, float] = Field(
description="Key numerical metrics"
)
recommendations: list[str] = Field(
description="Actionable recommendations"
)
confidence_score: float = Field(
ge=0, le=1,
description="Confidence in analysis (0-1)"
)
# Agent enforces output type
agent = Agent(
'anthropic:claude-3-5-sonnet-20241022',
result_type=AnalysisOutput,
)
result = agent.run_sync('Analyze Q4 sales data')
# result.data is guaranteed to be AnalysisOutput
print(result.data.executive_summary)
print(result.data.key_metrics)
print(result.data.recommendations)
Benefits:
- IDE autocomplete for all fields
- Runtime validation with clear errors
- Documentation in docstrings
- Type hints for function signatures
OpenAI Agents SDK
OpenAI Agents SDK uses Pydantic for structured outputs but with a different approach:
from openai import OpenAI
from pydantic import BaseModel
class AnalysisOutput(BaseModel):
executive_summary: str
key_metrics: dict[str, float]
recommendations: list[str]
confidence_score: float
client = OpenAI()
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": "You are a data analyst."},
{"role": "user", "content": "Analyze Q4 sales data"},
],
response_format={
"type": "json_schema",
"json_schema": {
"name": "analysis_output",
"schema": AnalysisOutput.model_json_schema(),
},
},
)
# Parse response
import json
data = json.loads(response.choices[0].message.content)
result = AnalysisOutput(**data)
Benefits:
- Works with any Pydantic model
- Clear separation of schema and data
- OpenAI's JSON Schema mode ensures compliance
Tool Calling
PydanticAI
Tools are defined as typed functions:
from pydantic_ai import Agent, RunContext
from pydantic import BaseModel
class SearchResult(BaseModel):
title: str
url: str
snippet: str
date: str
agent = Agent(
'openai:gpt-4o',
deps_type=dict, # Dependencies type
)
@agent.tool
async def search_web(ctx: RunContext[dict], query: str) -> list[SearchResult]:
"""Search the web for information."""
# Implementation
results = await perform_search(query)
return [SearchResult(**r) for r in results]
@agent.tool
async def fetch_page(ctx: RunContext[dict], url: str) -> str:
"""Fetch and return the content of a webpage."""
import httpx
async with httpx.AsyncClient() as client:
response = await client.get(url)
return response.text
# Use in conversation
result = agent.run_sync(
'Find recent articles about AI agents',
deps={'api_key': '...'},
)
Features:
- Type-safe function signatures
- Automatic schema generation
- Dependency injection via
RunContext - Async/sync support
OpenAI Agents SDK
Tools are defined with the OpenAI API:
from openai import OpenAI
from pydantic import BaseModel, Field
class SearchToolInput(BaseModel):
query: str = Field(description="Search query")
limit: int = Field(default=10, description="Max results")
client = OpenAI()
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "Search for AI agent frameworks"}],
tools=[{
"type": "function",
"function": {
"name": "search_web",
"description": "Search the web for information",
"parameters": SearchToolInput.model_json_schema(),
},
}],
)
# Handle tool call
tool_call = response.choices[0].message.tool_calls[0]
if tool_call.function.name == "search_web":
args = SearchToolInput(**json.loads(tool_call.function.arguments))
results = perform_search(args.query, args.limit)
Features:
- Native OpenAI tool calling
- JSON Schema validation
- Multiple tool types (function, file_search, code_interpreter)
- Streaming tool calls
Model Support
PydanticAI
Multi-provider support out of the box:
from pydantic_ai import Agent
# OpenAI
agent = Agent('openai:gpt-4o')
# Anthropic
agent = Agent('anthropic:claude-3-5-sonnet-20241022')
# Google Gemini
agent = Agent('google:gemini-1.5-pro')
# Groq
agent = Agent('groq:llama-3.1-405b')
# DeepSeek
agent = Agent('deepseek:deepseek-chat')
# Ollama (local)
agent = Agent('ollama:llama3.1')
# Custom endpoint
agent = Agent('openai:https://custom.api/v1', api_key='...')
Provider configuration:
# pydantic_ai/providers/openai.py
from pydantic_ai import Agent
from pydantic_ai.providers.openai import OpenAIProvider
provider = OpenAIProvider(
api_key="sk-...",
base_url="https://api.openai.com/v1",
)
agent = Agent(provider, model="gpt-4o")
OpenAI Agents SDK
OpenAI-first, with some flexibility:
from openai import OpenAI
# Standard OpenAI
client = OpenAI()
# Custom endpoint (Azure, etc.)
client = OpenAI(
api_key="...",
base_url="https://your-endpoint.openai.azure.com/",
)
# Via environment
# OPENAI_API_KEY and OPENAI_BASE_URL
client = OpenAI()
Limitations:
- Primarily designed for OpenAI models
- Azure OpenAI supported
- Other providers require custom clients
State Management
PydanticAI
Built-in conversation state:
from pydantic_ai import Agent
from pydantic import BaseModel
class ConversationState(BaseModel):
messages: list[dict]
context: dict
metadata: dict
agent = Agent(
'openai:gpt-4o',
result_type=str,
)
# State is managed automatically
result1 = agent.run_sync('What is the project about?')
result2 = agent.run_sync('Tell me more about the timeline')
# Both calls share context
# Access state
print(agent.state)
OpenAI Agents SDK
Thread-based state:
from openai import OpenAI
client = OpenAI()
# Create thread (state container)
thread = client.beta.threads.create(
metadata={"user_id": "123", "session": "abc"}
)
# Messages persist in thread
message1 = client.beta.threads.messages.create(
thread_id=thread.id,
role="user",
content="What is the project about?",
)
message2 = client.beta.threads.messages.create(
thread_id=thread.id,
role="user",
content="Tell me more about the timeline",
)
# Run uses full thread history
run = client.beta.threads.runs.create(
thread_id=thread.id,
assistant_id=assistant.id,
)
Multi-Agent Systems
PydanticAI
Dependencies-based multi-agent:
from pydantic_ai import Agent, RunContext
from pydantic import BaseModel
class ResearchResult(BaseModel):
findings: list[str]
sources: list[str]
class WritingResult(BaseModel):
article: str
word_count: int
# Research agent
research_agent = Agent(
'openai:gpt-4o',
result_type=ResearchResult,
)
# Writer agent depends on research agent
@research_agent.result_validator
def validate_result(ctx: RunContext, result: ResearchResult) -> ResearchResult:
# Can access other agents via deps
return result
writer_agent = Agent(
'openai:gpt-4o',
result_type=WritingResult,
deps_type=ResearchResult, # Depends on research result
)
@writer_agent.system_prompt
def add_research_context(ctx: RunContext[ResearchResult]) -> str:
return f"Based on research: {ctx.deps.findings}"
# Chain agents
research = research_agent.run_sync('Research quantum computing')
writing = writer_agent.run_sync(
'Write an article',
deps=research.data,
)
OpenAI Agents SDK
Assistant handoffs:
from openai import OpenAI
client = OpenAI()
# Research assistant
research_assistant = client.beta.assistants.create(
name="Research Assistant",
model="gpt-4o",
instructions="You are a research assistant.",
)
# Writer assistant
writer_assistant = client.beta.assistants.create(
name="Writer Assistant",
model="gpt-4o",
instructions="You are a technical writer.",
)
# Handoff via message
thread = client.beta.threads.create()
client.beta.threads.messages.create(
thread_id=thread.id,
role="user",
content="Research quantum computing and write an article",
)
# Run with assistant
run = client.beta.threads.runs.create(
thread_id=thread.id,
assistant_id=research_assistant.id,
)
# Handoff to writer
client.beta.threads.messages.create(
thread_id=thread.id,
role="assistant",
content="Here are my findings...",
assistant_id=research_assistant.id,
)
run2 = client.beta.threads.runs.create(
thread_id=thread.id,
assistant_id=writer_assistant.id,
)
Streaming
PydanticAI
Full streaming support:
from pydantic_ai import Agent
agent = Agent('openai:gpt-4o')
# Stream text
async for chunk in agent.run_stream('Explain quantum computing'):
print(chunk.data, end='', flush=True)
# Stream with events
async for event in agent.iter_events('Analyze this data'):
if event.type == 'text':
print(event.data, end='', flush=True)
elif event.type == 'tool_call':
print(f"\nCalling tool: {event.data.name}")
OpenAI Agents SDK
Streaming for completions and runs:
from openai import OpenAI
client = OpenAI()
# Stream chat completion
stream = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "Explain quantum computing"}],
stream=True,
)
for chunk in stream:
if chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end='', flush=True)
# Stream thread run
stream = client.beta.threads.runs.create(
thread_id=thread.id,
assistant_id=assistant.id,
stream=True,
)
for event in stream:
if event.event == 'thread.message.delta':
print(event.data.delta.content[0].text.value, end='', flush=True)
Error Handling
PydanticAI
Type-safe error handling:
from pydantic_ai import Agent, ModelRetry
agent = Agent('openai:gpt-4o')
@agent.result_validator
def validate_result(result):
if not result.findings:
raise ModelRetry('No findings returned, please try again')
if len(result.sources) < 2:
raise ModelRetry('Need more sources for credibility')
return result
# Handle errors
try:
result = agent.run_sync('Research something obscure')
except ModelRetry as e:
print(f"Retry needed: {e}")
except Exception as e:
print(f"Error: {e}")
OpenAI Agents SDK
Standard error handling:
from openai import OpenAI, APIError, RateLimitError
client = OpenAI()
try:
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "Hello"}],
)
except RateLimitError as e:
print(f"Rate limited: {e}")
# Implement retry logic
except APIError as e:
print(f"API error: {e}")
except Exception as e:
print(f"Unexpected error: {e}")
Testing
PydanticAI
Built-in testing utilities:
from pydantic_ai import Agent
from pydantic_ai.test import MockModel
agent = Agent('openai:gpt-4o')
# Mock model for testing
mock = MockModel()
mock.add_chat_completion('Hello', 'Hi there!')
agent_test = Agent('test', deps_type=None)
agent_test._model = mock
result = agent_test.run_sync('Say hello')
assert result.data == 'Hi there!'
OpenAI Agents SDK
Use OpenAI's test utilities or mock the client:
from unittest.mock import Mock, patch
from openai import OpenAI
@patch('openai.OpenAI')
def test_assistant_creation(mock_client):
client = mock_client.return_value
client.beta.assistants.create.return_value = Mock(id="asst_123")
# Test code
assistant = client.beta.assistants.create(...)
client.beta.assistants.create.assert_called_once()
Best Use Cases
Choose PydanticAI When:
- ✅ Type safety is critical: You want compile-time guarantees
- ✅ Multi-provider support: Need to switch between model providers
- ✅ Complex data structures: Results have many fields
- ✅ IDE experience: Want excellent autocomplete and type hints
- ✅ Pydantic ecosystem: Already using Pydantic in your project
- ✅ Local models: Want to use Ollama or other local models
Choose OpenAI Agents SDK When:
- ✅ OpenAI ecosystem: Deep integration with OpenAI services
- ✅ Simplicity: Want minimal boilerplate
- ✅ Assistants API: Need full Assistants API features
- ✅ File search: Need built-in file search capabilities
- ✅ Production OpenAI: Already using OpenAI in production
- ✅ Quick prototyping: Want to get started fast
Performance Comparison
| Metric | PydanticAI | OpenAI Agents SDK |
|---|---|---|
| Startup time | Fast (lightweight) | Moderate (more dependencies) |
| Memory usage | Low | Moderate |
| Request latency | Same (depends on API) | Same (depends on API) |
| Streaming overhead | Minimal | Minimal |
| Validation overhead | Small (Pydantic) | Small (JSON Schema) |
Migration Guide
From PydanticAI to OpenAI Agents SDK
# PydanticAI
from pydantic_ai import Agent
agent = Agent('openai:gpt-4o', result_type=MyResult)
result = agent.run_sync('Query')
# OpenAI Agents SDK
from openai import OpenAI
client = OpenAI()
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "Query"}],
response_format={"type": "json_schema", ...},
)
From OpenAI Agents SDK to PydanticAI
# OpenAI Agents SDK
from openai import OpenAI
client = OpenAI()
response = client.chat.completions.create(...)
# PydanticAI
from pydantic_ai import Agent
agent = Agent('openai:gpt-4o')
result = agent.run_sync('Query')
Conclusion
Both frameworks are excellent choices for building AI agents in Python. The decision comes down to your specific needs:
- PydanticAI is ideal for developers who value type safety, multi-provider support, and the Pydantic ecosystem.
- OpenAI Agents SDK is ideal for teams deeply invested in the OpenAI ecosystem who want simplicity and native OpenAI features.
For most new projects, I recommend PydanticAI for its superior type safety and flexibility, unless you have specific requirements for OpenAI's Assistants API.
Resources
- PydanticAI Documentation: https://ai.pydantic.dev/
- PydanticAI GitHub: https://github.com/pydantic/pydantic-ai
- OpenAI Agents SDK: https://platform.openai.com/docs/assistants/overview
- OpenAI Python SDK: https://github.com/openai/openai-python
Last updated: May 2026
