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

FeaturePydanticAIOpenAI Agents SDK
MaintainerPydantic (Samuel Colvin)OpenAI
Type Safety⭐⭐⭐⭐⭐ Excellent (Pydantic)⭐⭐⭐⭐ Very Good
Model SupportMulti-provider (OpenAI, Anthropic, Google, Groq, DeepSeek)OpenAI models primarily
Structured OutputsNative Pydantic integrationNative Pydantic support
Tool CallingType-safe with Pydantic modelsFirst-class tool support
State ManagementBuilt-in conversation stateBuilt-in state management
StreamingFull streaming supportFull streaming support
Multi-AgentSupported via dependenciesSupported via handoffs
Learning CurveModerate (Pydantic knowledge)Low (OpenAI API familiarity)
DocumentationExcellentExcellent
CommunityGrowingLarge (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

MetricPydanticAIOpenAI Agents SDK
Startup timeFast (lightweight)Moderate (more dependencies)
Memory usageLowModerate
Request latencySame (depends on API)Same (depends on API)
Streaming overheadMinimalMinimal
Validation overheadSmall (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


Last updated: May 2026