Course
Building AI-powered applications often leads to unstructured outputs, type mismatches, and production reliability issues. Traditional approaches to integrating LLMs into Python applications lack the structure and validation needed for production systems. Pydantic AI solves this by combining Pydantic’s data validation with an agent framework for LLM interactions.
In this tutorial, you’ll learn how to create structured AI agents that produce validated outputs and maintain type safety. We’ll cover setting up agents with system prompts, building function tools that LLMs can call during conversations, and implementing structured output validation. You’ll also discover how to manage dependencies across agent components and stream responses for real-time applications. By the end, you’ll have hands-on experience building AI applications that handle complex workflows.
If you’re looking for a hands-on course on building agentic AI systems, I recommend the Building Multi-Agent Systems with LangGraph course.
What is Pydantic AI?

Pydantic AI is a Python agent framework that brings structure and type safety to LLM applications. Unlike basic LLM integrations that return raw text, Pydantic AI treats AI interactions as structured conversations with validated inputs and outputs, making it easier to build reliable applications.
Key features and benefits:
- Structured output validation — Your AI responses automatically conform to Pydantic models, preventing parsing errors and ensuring data consistency
- Function tools — LLMs can call your Python functions during conversations, giving them access to real data and computations
- Type safety — Full typing support means fewer runtime errors and better IDE assistance when building complex AI workflows
- System prompts — Define clear instructions for your AI agents that stay consistent across all interactions
- Dependency injection — Share context, database connections, and user preferences across all agent components cleanly
- Multiple execution modes — Run agents synchronously, asynchronously, or stream responses in real-time based on your needs
- Agent reusability — Create agents once and reuse them throughout your application, similar to FastAPI routers
This approach changes AI from unpredictable text generation into reliable, typed interactions that fit naturally into existing Python applications. Instead of wrestling with prompt engineering and output parsing, you can focus on building the actual functionality your users need.
Prerequisites
Before diving into this tutorial, you’ll need a few things:
Required knowledge:
- Python fundamentals : Become comfortable with classes and functions
- Basics of asynchronous programming in Python
- Pydantic basics : Understand models, field validation, and type annotations
- LLM familiarity : Basic knowledge of how language models work and what prompts are
Helpful but not required:
- Agent concepts : Some exposure to AI agents and tool calling, though we’ll cover this
Setup requirements:
- Python 3.9 or higher
- OpenAI and Anthropic API keys
- Basic terminal/command line skills for package installation
If you’re new to Pydantic, their official documentation covers the basics well. For LLM fundamentals, any introductory guide to ChatGPT or similar models will give you enough background to follow along.
Getting Started with Pydantic AI
Throughout this tutorial, we’ll build a data analysis agent that helps us understand sales performance across different regions and products. This agent will start simple and grow more sophisticated as we add features like custom tools, structured outputs, and streaming capabilities.
Installation and setup
Before diving into code, let’s install Pydantic AI and set up our environment:
pip install pydantic-aiYou’ll also need both an OpenAI and Anthropic API key. Set them as environment variables:
export OPENAI_API_KEY="your-api-key-here"export ANTHROPIC_API_KEY="your-api-key-here"> We will be using both OpenAI and Anthropic models as there are some features in Pydantic AI that are supported in one but not the other.
For our data analysis examples, we’ll also use pandas for data manipulation:
pip install pandasCreating your first agent
Let’s start with the simplest possible agent — one that can answer basic questions about sales data. In Pydantic AI, an agent is your main interface for LLM interactions (run the following code in a Python script):
from pydantic_ai import Agent# Create a basic sales analysis agentsales_agent = Agent( 'openai:gpt-4o', system_prompt=( "You are a data analyst specializing in sales performance. " "Provide clear, actionable insights based on the data provided." ))# Run the agent with a simple questionresult = sales_agent.run_sync("What are the key metrics I should track for sales performance? Answer in two sentences.")print(result.output)Output:
Key metrics to track for sales performance include conversion rate, which measures the percentage of leads that result in sales, and average deal size, indicating the average revenue earned per transaction. Additionally, monitoring customer acquisition cost (CAC) and customer lifetime value (CLV) helps evaluate the efficiency and profitability of sales efforts.This creates an agent that understands its role as a sales analyst and can provide relevant advice. The system_prompt gives the LLM context about what kind of responses you want, while run_sync() executes the agent and returns a result.
Here, the run_sync() function only works in Python scripts. To run agents in a Jupyter Notebook, you can use the following syntax:
result = await agent.run("...")print(result.output)Changing the LLM and its settings
Pydantic AI supports multiple LLM providers and allows you to customize model behavior. Here’s how to experiment with different options:
# Creating agents with different modelsclaude_agent = Agent('anthropic:claude-3-5-sonnet-20241022', system_prompt="You are a sales analyst.")gpt_mini_agent = Agent('openai:gpt-4o-mini', system_prompt="You are a sales analyst.")Each model brings different capabilities to your data analysis workflow. Claude models excel at detailed reasoning and complex analysis, making them ideal when you need thorough insights from your sales data. GPT-4o-mini offers faster responses and lower costs, perfect for quick questions or when processing large volumes of queries.
Beyond choosing different models, you can fine-tune their behavior using model settings:
# Add model-specific settingsconfigured_agent = Agent( 'openai:gpt-4o', system_prompt="You are a sales analyst.", model_settings={ 'temperature': 0.3, # More focused, less creative responses 'max_tokens': 500, # Limit response length })Model settings you can customize:
- temperature — Controls creativity (0.0 = focused, 1.0 = creative)
- max_tokens — Limits response length
- top_p — Alternative to temperature for controlling randomness
For data analysis tasks, lower temperature values (0.1–0.3) typically work better since you want consistent, factual responses rather than creative interpretations. Setting max_tokens helps control response length, which is useful when you need concise summaries or have API cost concerns.
Using Tools in Agents
So far, our sales analysis agent can only work with the information we provide in our prompts. But real data analysis often requires accessing external information, running calculations, or connecting to business systems. This is where tools come in — they give your agent the ability to take actions and gather information during conversations.
Built-in tools
Built-in tools are ready-to-use functions provided by LLM providers. Pydantic AI supports web search and code execution tools that run directly on the provider’s infrastructure.
Let’s start with web search, which is perfect for getting current market information:
from pydantic_ai import Agent, WebSearchTool# Create an agent with web search abilitiesmarket_research_agent = Agent( 'anthropic:claude-3-5-sonnet-20241022', builtin_tools=[WebSearchTool()], system_prompt=( "You are a sales analyst. Use web search to find current information. " "Keep responses to 3-4 sentences maximum." ))# Get current market informationresult = market_research_agent.run_sync( "Search for 2025 laptop sales trends and average selling prices. Give me the main trends.")print(result.output)Let me search for current laptop sales trends.Based on the search results, here are the key laptop market trends for 2025:…This changes your data analysis from working with old information to including real-time market data. Instead of analyzing data alone, your agent can now compare your company’s performance against current industry trends.
Now let’s add code execution for mathematical analysis and data processing:
from pydantic_ai import Agent, CodeExecutionTool# Agent that can run calculations and data analysiscalculation_agent = Agent( 'anthropic:claude-sonnet-4-0', builtin_tools=[CodeExecutionTool()], system_prompt=( "You are a data analyst. Use code to perform calculations and create visualizations. " "Show your work and provide clear explanations." ))# Analyze sales data with calculationsresult = calculation_agent.run_sync("""I have sales data for Q1 2025:- North region: 150 laptops at $1,500 each, 320 phones at $500 each- South region: 89 laptops at $1,500 each, 245 phones at $500 each - East region: 201 laptops at $1,500 each, 180 phones at $500 each- West region: 167 laptops at $1,500 each, 290 phones at $500 eachCalculate total revenue by region and create a simple chart showing the results.""")print(result.output)```plaintext## SummaryBased on the Q1 2025 sales data analysis, here are the key findings:**Total Revenue by Region:**- **West Region**: $395,500 (highest)- **East Region**: $391,500- **North Region**: $385,000- **South Region**: $256,000 (lowest)Code execution moves beyond simple text responses to actual computational analysis. Your agent can now perform complex calculations, create visualizations, and process data that would be impossible through conversation alone. The result object contains both the agent's analysis and any files generated during the code execution, which you can download for further use.
Writing custom tools
While built-in tools are useful, you’ll often need custom tools that work with your specific business systems. Custom tools are Python functions that your agent can call during conversations.
Let’s create tools that calculate common sales metrics:
from pydantic_ai import Agent# Create an agent with custom business toolssales_agent = Agent( 'anthropic:claude-sonnet-4-0', system_prompt=( "You are a sales analyst. Use tools to calculate metrics and analyze business data. " "Give concise answers in 3-4 sentences maximum." ))@sales_agent.tool_plaindef calculate_conversion_rate(leads: int, sales: int) -> str: """Calculate conversion rate from leads to sales.""" if leads == 0: return "Cannot calculate conversion rate: no leads provided" conversion_rate = (sales / leads) * 100 return f"Conversion rate: {conversion_rate:.2f}% ({sales} sales from {leads} leads)"@sales_agent.tool_plaindef calculate_average_order_value(total_revenue: float, total_orders: int) -> str: """Calculate average order value.""" if total_orders == 0: return "Cannot calculate AOV: no orders provided" aov = total_revenue / total_orders return f"Average Order Value: ${aov:.2f} (${total_revenue:,.0f} revenue from {total_orders} orders)"# Use the custom toolsresult = sales_agent.run_sync( "Calculate conversion rate for 500 leads that resulted in 75 sales, and AOV for $180,000 revenue from 120 orders")print(result.output)Your conversion rate is 15.00%, meaning you're successfully converting 75 out of every 500 leads into sales. The average order value is $1,500.00, indicating strong revenue per transaction. These metrics suggest effective sales processes with high-value customers, though there's room to improve lead conversion rates.These custom tools handle specific sales calculations that your business uses regularly. The conversion rate tool helps measure marketing performance, while the average order value tool tracks customer spending patterns. Custom tools connect your agent with your specific business logic and systems.
For more advanced tool patterns, including dependency injection and error handling, check out the complete tools documentation.
Using Structured Outputs
So far our agents have returned plain text responses, which work well for exploration but create problems in production applications. When you need to process agent responses programmatically, parse specific data points, or integrate AI outputs with other systems, you need guaranteed structure.
Let’s say you want to analyze sales data and always get back specific metrics. With plain text, the AI might return different formats each time. With structured outputs, you define exactly what you want:
from pydantic import BaseModelfrom pydantic_ai import Agentclass SalesInsight(BaseModel): total_revenue: float best_performing_region: str worst_performing_region: str recommendation: str# Create an agent that returns structured analysisanalysis_agent = Agent( 'anthropic:claude-sonnet-4-0', output_type=SalesInsight, system_prompt="Analyze sales data and provide structured insights with clear recommendations.")# Get structured analysisresult = analysis_agent.run_sync("""Q1 2025 sales data:- North: $385,000 revenue (470 units)- South: $256,000 revenue (334 units) - East: $391,500 revenue (381 units)- West: $395,500 revenue (457 units)Analyze this data and provide insights.""")print(f"Total Revenue: ${result.output.total_revenue:,.0f}")print(f"Best Region: {result.output.best_performing_region}")print(f"Recommendation: {result.output.recommendation}")Total Revenue: $1,428,000Best Region: WestRecommendation: Focus on improving South region performance through targeted marketing and sales training, as it significantly underperforms with only $256,000 revenue. The West region's success ($395,500) should be analyzed and replicated across other regions. Additionally, investigate why East region has the lowest average selling price per unit ($1,028) compared to West's highest ($865) - there may be pricing strategy opportunities.Notice the output_type=SalesInsight parameter. This tells the agent it must return a SalesInsight object with exactly those four fields. No more, no less. The result comes back as result.output which is automatically validated and typed.
Structured outputs work by converting your Pydantic model into a JSON schema that the LLM follows. The AI can’t deviate from your structure — if it tries to return something else, Pydantic AI will ask it to try again.
For more complex analysis scenarios, you can nest models and use lists:
from typing import Listfrom pydantic import BaseModelclass RegionalMetrics(BaseModel): region: str revenue: float units_sold: int average_price: float performance_rating: str # "excellent", "good", "needs_improvement"class ComprehensiveAnalysis(BaseModel): total_revenue: float total_units: int regional_breakdown: List[RegionalMetrics] top_performer: str areas_for_improvement: List[str] quarterly_grade: str# Agent with detailed structured outputdetailed_agent = Agent( 'anthropic:claude-sonnet-4-0', output_type=ComprehensiveAnalysis, system_prompt=( "Provide comprehensive sales analysis with detailed regional metrics. " "Rate each region's performance and give an overall quarterly grade (A-F)." ))result = detailed_agent.run_sync("""Analyze Q1 2025 performance:- North: $385k revenue, 470 units- South: $256k revenue, 334 units- East: $391.5k revenue, 381 units - West: $395.5k revenue, 457 units""")# Access structured datafor region in result.output.regional_breakdown: print(f"{region.region}: {region.performance_rating} (${region.average_price:.2f} avg price)")print(f"Quarterly Grade: {result.output.quarterly_grade}")North: B ($819.15 avg price)South: C ($766.47 avg price)East: A ($1027.56 avg price)West: A- ($865.43 avg price)Quarterly Grade: B+This approach changes AI from unpredictable text generation into reliable data processing. You can store each regional metric in a database, trigger alerts based on performance ratings, or generate executive dashboards automatically. No regex parsing, no handling different response formats — just clean, validated data every time.
The output_type parameter accepts any Pydantic model, Python dataclass, TypedDict, or even simple types like int or list[str]. For data analysis workflows, this means you can build reliable pipelines where AI analysis feeds directly into your existing business systems.
Messages and Chat History in Pydantic Agents
When working with data analysis agents, you often need conversations that span multiple interactions. Maybe you start by asking for sales trends, then want to dig deeper into specific regions, or compare different time periods. Messages and chat history let you maintain context across these conversations.
Accessing message history
Every time you run an agent, Pydantic AI keeps track of the entire conversation. You can access this history to understand what happened or continue the conversation later:
from pydantic_ai import Agent# Create our sales analysis agentsales_agent = Agent( 'anthropic:claude-sonnet-4-0', system_prompt="You are a sales analyst. Provide clear, concise analysis.")# First questionresult1 = sales_agent.run_sync("What are the main KPIs I should track for Q1 2025 sales?")print(result1.output)# Access the conversation historyall_messages = result1.all_messages()print(f"Total messages in conversation: {len(all_messages)}")# Just the new messages from this runnew_messages = result1.new_messages()print(f"New messages from this run: {len(new_messages)}")Here are the essential KPIs to track for Q1 2025 sales:...Total messages in conversation: 2New messages from this run: 2The all_messages() method gives you everything - system prompts, user questions, and agent responses. The new_messages() method only returns what happened in the current run. This is useful when you're building longer conversations and want to track what changed.
Continuing conversations with message history
To build on previous conversations, pass the message history to your next agent run. This lets the agent remember what you discussed before:
# Continue the conversation from where we left offresult2 = sales_agent.run_sync( "How should I calculate conversion rates for each of those KPIs?", message_history=result1.all_messages())print(result2.output)# The agent now has context from both interactionsprint(f"Full conversation length: {len(result2.all_messages())}")# Ask a follow-up that references the entire conversationresult3 = sales_agent.run_sync( "Which of these metrics would be most important for a monthly executive report?", message_history=result2.all_messages())print(result3.output)Each new run builds on the previous context. The agent remembers not just your questions, but its own previous responses, creating a coherent analytical conversation.
Storing and loading messages to JSON
For longer-term storage or sharing conversations between systems, you can serialize the message history to JSON:
import json# Get the conversation as JSONconversation_json = result3.all_messages_json()# Save to filewith open('sales_analysis_conversation.json', 'w') as f: f.write(conversation_json.decode('utf-8'))print("Conversation saved to JSON file")# Load it back laterwith open('sales_analysis_conversation.json', 'r') as f: loaded_conversation = f.read()# You can now use this loaded conversation in a new agent run# (Note: You'd need to convert back from JSON to message objects for actual use)print(f"Loaded conversation size: {len(loaded_conversation)} characters")The all_messages_json() method returns the conversation as JSON bytes, perfect for saving to databases, sending over APIs, or archiving analysis sessions. There's also new_messages_json() if you only want to save the latest part of the conversation.
This approach lets you build persistent analytical workflows. You could save each client’s analysis session, resume complex data investigations across multiple days, or share conversation context between different team members working on the same analysis.
Working With Images and Documents
Real data analysis often involves more than just text and numbers. You might need to analyze charts from reports, extract data from PDFs, or interpret graphs and visualizations. Pydantic AI supports image and document input, letting your agents work with these rich data sources directly.
Analyzing images from URLs
When you have charts, graphs, or visualizations hosted online, you can point your agent directly to them:
from pydantic_ai import Agent, ImageUrl# Create an agent that can analyze visual datavisual_agent = Agent( 'anthropic:claude-sonnet-4-0', system_prompt="You are a data analyst who can interpret charts, graphs, and business documents.")# Analyze a sales chart from a URLresult = visual_agent.run_sync([ "What are the main trends shown in this sales chart? Give me 3 key takeaways.", ImageUrl(url='https://example.com/q1-sales-chart.png')])print(result.output)The agent can identify chart types, read values from axes, spot trends, and provide business insights just like a human analyst would when looking at the same visualization.
Working with local images
For images stored locally — like screenshots from your BI tools or charts exported from Excel — use BinaryContent:
from pathlib import Pathfrom pydantic_ai import Agent, BinaryContent# Load a local sales dashboard screenshotdashboard_path = Path('sales_dashboard_jan_2025.png')result = visual_agent.run_sync([ "Analyze this sales dashboard. What metrics need attention based on what you see?", BinaryContent( data=dashboard_path.read_bytes(), media_type='image/png' )])print(result.output)This approach works great for automated reporting workflows. You could screenshot your dashboards programmatically, then have the AI provide written analysis to accompany the visuals.
Analyzing documents from URLs
Many business documents are shared via URLs — reports on company intranets, PDFs in cloud storage, or public research papers that inform your analysis:
from pydantic_ai import Agent, DocumentUrl# Create an agent for document analysisdoc_agent = Agent( 'anthropic:claude-sonnet-4-0', system_prompt="Extract and summarize business-relevant insights from documents.")# Analyze a quarterly report PDFresult = doc_agent.run_sync([ "Read this quarterly earnings report and summarize the revenue trends and outlook.", DocumentUrl(url='https://company.com/reports/q1-2025-earnings.pdf')])print(result.output)The agent can read through entire documents, extract relevant data points, and provide summaries that would take humans much longer to compile.
Processing local documents
For documents on your local system — like exported reports, contracts, or internal presentations — use BinaryContent:
from pathlib import Pathfrom pydantic_ai import Agent, BinaryContent# Load a local Excel export saved as PDFreport_path = Path('monthly_sales_analysis_jan_2025.pdf')result = doc_agent.run_sync([ "Extract the key performance metrics from this monthly sales report and identify any concerning trends.", BinaryContent( data=report_path.read_bytes(), media_type='application/pdf' )])print(result.output)This opens up powerful automation possibilities. You could process stacks of reports, extract data from invoices, or analyze contract terms — all tasks that normally require manual review.
Important notes:
Different models support different file formats. Most handle common image formats (PNG, JPEG) and PDFs, but check your model’s documentation for specifics. Some models like Google’s Vertex AI can access cloud storage URLs directly, while others download the content first.
For data analysis workflows, this multimodal capability bridges the gap between your visual reports and AI-powered insights, making it possible to automate analysis of the full range of business documents you work with daily.
Enabling Streaming in Pydantic AI
When working with complex data analysis tasks, you want to see results as they’re generated rather than waiting for the complete response. Streaming lets users watch the analysis unfold in real-time, which is valuable when asking detailed questions about sales performance.
Pydantic AI provides built-in streaming through the run_stream() method. Here's how it works:
import asynciofrom pydantic_ai import Agent# Create our sales analysis agentsales_agent = Agent( 'anthropic:claude-sonnet-4-0', system_prompt=( "You are a data analyst specializing in sales performance. " "Provide detailed, actionable insights based on the data provided." ))async def demo_streaming(): question = """ Analyze this sales scenario: Our company sold 1,200 laptops and 2,300 phones last quarter across 4 regions. Laptops average $1,500 each, phones $500 each. What are the key insights and recommendations? """ async with sales_agent.run_stream(question) as result: async for text in result.stream_text(): print(text, end='', flush=True)# Run the streaming analysisasyncio.run(demo_streaming())Understanding the streaming syntax:
The async with creates a connection to the LLM that stays open while receiving text chunks. Think of it like opening a phone call - you need to establish the connection, have your conversation, then hang up properly.
The async for loop processes each text chunk as it arrives. Note that stream_text() regenerates the complete text each time - so you get "Hello", then "Hello world", then "Hello world, how". This gives you the full context but can be repetitive for display purposes.
Why async and asyncio work together:
When you ask an LLM a question, it takes time to generate the response — usually 3–10 seconds. During this time, the model sends back small pieces of text every 50–200 milliseconds.
Without async, your program would be completely frozen waiting for each piece. With async, Python’s event loop (the system that manages these waiting periods) can switch between tasks. This means your program stays responsive and can even run multiple conversations at once.
asyncio.run() is what starts this whole process - it creates the event loop that coordinates everything. When you call asyncio.run(demo_streaming()), it sets up the environment where your async functions can pause and resume as needed while waiting for LLM responses.
Now let’s create a reusable function that shows just the new text deltas for a better streaming experience:
from pydantic_ai.messages import PartDeltaEvent, TextPartDeltaasync def stream_agent_response(agent: Agent, task: str): """ Stream a response from any Pydantic AI agent showing just new text deltas. """ async def event_handler(ctx, event_stream): async for event in event_stream: if isinstance(event, PartDeltaEvent) and isinstance( event.delta, TextPartDelta ): print(event.delta.content_delta, end="", flush=True) # Use agent.run() with event_stream_handler for proper streaming result = await agent.run(task, event_stream_handler=event_handler) print() # Add a newline when streaming is complete return result# Usage exampleresponse = asyncio.run(stream_agent_response( sales_agent, "What are the top 3 metrics I should track for quarterly sales analysis?"))Breaking down the streaming function:
The stream_agent_response() function creates a more natural streaming experience by showing only new text as it arrives. Here's how it works:
- Event handler setup: The inner
event_handler()function listens for specific events during the agent's processing. It filters forPartDeltaEventwithTextPartDelta- these represent new pieces of text being generated. - Delta content: Instead of getting the full regenerated text each time,
event.delta.content_deltagives us just the new words or characters. This creates a typewriter effect where you see the response being written word by word. - Agent execution: We use
agent.run()with theevent_stream_handlerparameter. This tells the agent to call our custom handler for each streaming event, giving us fine-grained control over what gets displayed. - Real-time output: The
print(event.delta.content_delta, end="", flush=True)displays each new piece immediately without adding line breaks, creating smooth flowing text.
This approach gives you the natural feel of watching the AI think through the analysis in real-time, rather than waiting for a complete response or seeing repetitive regenerated text. We will use this function in the coming sections to show streaming responses throughout our data analysis examples.
Conclusion
Throughout this tutorial, you’ve built a complete understanding of Pydantic AI through practical data analysis examples. You’ve learned how to create agents with system prompts, add both built-in and custom tools, structure outputs with Pydantic models, and stream responses for real-time feedback. These components combine to create AI systems that act more like analytical partners than simple chatbots.
The applications go well beyond the sales analysis examples we’ve covered. You can now build AI agents that read your specific data sources, follow your business rules, and return results in formats that work with your existing systems. Whether you’re automating report generation, analyzing customer feedback, or processing financial data, Pydantic AI gives you the structure and reliability needed for production applications.
If you’re interested in building multi-agent systems with LangGraph, be sure to check out our hands-on course.

I am a data science content creator with over 2 years of experience and one of the largest followings on Medium. I like to write detailed articles on AI and ML with a bit of a sarcastıc style because you've got to do something to make them a bit less dull. I have produced over 130 articles and a DataCamp course to boot, with another one in the makıng. My content has been seen by over 5 million pairs of eyes, 20k of whom became followers on both Medium and LinkedIn.

