Pydantic AI is a Python framework for building LLM agents that return validated, structured outputs using Pydantic models. Instead of parsing raw strings from LLMs, you get type-safe objects with automatic validation.
If you’ve used FastAPI or Pydantic before, then you’ll recognize the familiar pattern of defining schemas with type hints and letting the framework handle the type validation for you.
By the end of this tutorial, you’ll understand that:
- Pydantic AI uses
BaseModelclasses to define structured outputs that guarantee type safety and automatic validation. - The
@agent.tooldecorator registers Python functions that LLMs can invoke based on user queries and docstrings. - Dependency injection with
deps_typeprovides type-safe runtime context like database connections without using global state. - Validation retries automatically rerun queries when the LLM returns invalid data, which increases reliability but also API costs.
- Google Gemini, OpenAI, and Anthropic models support structured outputs best, while other providers have varying capabilities.
Before you invest time learning Pydantic AI, it helps to understand when it’s the right tool for your project. This decision table highlights common use cases and what to choose in each scenario:
| Use Case | Pydantic AI | If not, look into … |
|---|---|---|
| You need structured, validated outputs from an LLM | ✅ | - |
| You’re building a quick prototype or single-agent app | ✅ | - |
| You already use Pydantic or FastAPI | ✅ | - |
| You need a large ecosystem of pre-built integrations (vector stores, retrievers, and so on) | - | LangChain or LlamaIndex |
| You want fine-grained control over prompts with no framework overhead | - | Direct API calls |
Pydantic AI emphasizes type safety and minimal boilerplate, making it ideal if you value the FastAPI-style development experience.
Get Your Code: Click here to download the free sample code you’ll use to work with Pydantic AI and build type-safe LLM agents in Python.
Take the Quiz: Test your knowledge with our interactive “Pydantic AI: Build Type-Safe LLM Agents in Python” quiz. You’ll receive a score upon completion to help you track your learning progress:
Interactive Quiz
Pydantic AI: Build Type-Safe LLM Agents in PythonLearn the trade-offs of using Pydantic AI in production, including validation retries, structured outputs, tool usage, and token costs.
Start Using Pydantic AI to Create Agents
Before you dive into building agents with Pydantic AI and Python, you’ll need to install it and set up an API key for your chosen language model provider. For this tutorial, you’ll use Google Gemini, which offers a free tier perfect for experimentation.
Note: Pydantic AI is LLM-agnostic and supports multiple AI providers. Check the Model Providers documentation page for more details on other providers.
You can install Pydantic AI from the Python Package Index (PyPI) using a package manager like pip. Before running the command below, you should create and activate a virtual environment:
(venv) $ python -m pip install pydantic-ai
This command installs all supported model providers, including Google, Anthropic, and OpenAI. From this point on, you just need to set up your favorite provider’s API key to use their models with Pydantic AI. Note that in most cases, you’d need a paid subscription to get a working API key.
Note: You can also power your Pydantic AI apps with local language models. To do this, you can use Ollama with your favorite local models. In this scenario, you won’t need to set up an API key.
If you prefer a minimal installation with only Google Gemini support, you can install the slim package instead:
(venv) $ python -m pip install "pydantic-ai-slim[google]"
You need a personal Google account to use the Gemini free tier. You’ll also need a Google API key to run the examples in this tutorial, so head over to ai.google.dev to get a free API key.
Once you have the API key, set it as an environment variable:
With the installation complete and your API key configured, you’re ready to create your first agent. The Python professionals on Real Python’s team have technically reviewed and tested all the code examples in this tutorial, so you can work through them knowing they run as shown.
Here’s a minimal agent example to get you started with Pydantic AI:
>>> from pydantic_ai import Agent
>>> agent = Agent(
... "google-gla:gemini-2.5-flash",
... instructions="You're a Python Expert. Reply in one sentence.",
... )
>>> result = agent.run_sync("What is Pydantic AI?")
>>> print(result.output)
Pydantic AI refers to using the Pydantic library to define, validate,
and structure data for artificial intelligence applications, especially
Large Language Models, to ensure reliable input/output and function calling.
In this example, you first import the Agent class from pydantic_ai. Agents are the primary interface for interacting with LLMs. Then, you instantiate the class with an LLM and some instructions as arguments.
Note: Pydantic AI distinguishes between system prompts and instructions. As per the documentation, you should use:
instructionswhen you want your request to the model to only include system prompts for the current agentsystem_promptwhen you want your request to the model to retain the system prompts used in previous requests (possibly made using other agents) (Source)
In this tutorial, you’ll use instructions because this parameter is a better fit for single-agent use cases.
The first argument to Agent specifies the target model using the format "provider:model". In this example, "google-gla:gemini-2.5-flash" tells Pydantic AI to use Google’s Gemini 2.5 Flash model.
Next, you call .run_sync() on the agent instance with a user prompt. This method queries the model synchronously and returns the result that you print to the screen.
Note: All examples in this tutorial run synchronously using .run_sync() for simplicity. Production applications may need to use .run() with async and await instead:
result = await agent.run("Your query here")
The .run_sync() method is convenient for scripts and learning. For applications that execute heavy I/O-bound tasks, consider using .run() with async/await for better concurrency.
By default, the agent returns a string. While this works for simple queries, the real power of Pydantic AI comes from structured outputs, which you’ll explore in the following section.
Return Structured, Validated Data With Pydantic Models
Raw string responses from LLMs can be problematic in some use cases. You may need to parse them, extract structured data, and handle cases where the LLM returns unexpected formats. Pydantic AI solves these issues by allowing you to define the exact structure of the agent output using Pydantic models.
Note: Don’t confuse Pydantic models with language models. Pydantic models are classes that inherit from BaseModel and are one of the primary ways of defining data schemas.
Here’s a quick agent example that returns structured data with city information:
>>> from pydantic import BaseModel
>>> from pydantic_ai import Agent
>>> class CityInfo(BaseModel):
... name: str
... country: str
... population: int
... fun_fact: str
...
>>> agent = Agent(
... "google-gla:gemini-2.5-flash",
... output_type=CityInfo
... )
>>> result = agent.run_sync("Tell me about Tokyo")
>>> result.output
CityInfo(
name='Tokyo',
country='Japan',
population=13960000,
fun_fact='Tokyo has the most Michelin stars of any city in the world.'
)
>>> print(f"{result.output.name}, {result.output.country}")
Tokyo, Japan
>>> print(f"Population: {result.output.population:,}")
Population: 13,960,000
>>> print(f"Fun fact: {result.output.fun_fact}")
Fun fact: Tokyo has the most Michelin stars of any city in the world.
The CityInfo class inherits from BaseModel and defines the data schema for the agent’s response. Each field has a type hint: name and country are strings, population is an integer, and fun_fact is also a string.
When you pass CityInfo to the Agent using the output_type argument, Pydantic AI instructs the language model to return data that matches the provided schema.
Behind the scenes, Pydantic AI converts your Pydantic model, CityInfo, into a JSON schema that the LLM understands. The language model generates a response matching this schema, and Pydantic validates it.
If the response doesn’t match the schema, Pydantic catches the error, and Pydantic AI automatically retries the request. You can use the output_retries argument to set the number of retries specifically for output validation, or retries to set the default retry count for the agent overall.
You can access the validated data through result.output, which gives you a CityInfo instance with all the type-safety benefits:
- Each field in the resulting
CityInfohas the correct type. - Your IDE provides autocomplete for fields.
- Type checkers catch errors before runtime.
- You’re confident that your data matches the expected schema.
This approach eliminates any string-parsing code you’d otherwise need to write. Instead of relying on regular expressions and error-prone string splitting, you define your schema once and let Pydantic AI handle validation and retries for you. If you want to get more comfortable with Pydantic models before moving on, Real Python’s video course on Pydantic data validation walks you through schemas, validators, and type coercion step by step.
Leverage Your Agent’s Function Calling Capabilities
Language models can’t directly access external systems like databases, APIs, files, or even the terminal. Fortunately, some models offer function calling capabilities. This feature allows you to register Python functions as tools that agents can invoke. The LLM decides whether to call one or several of your functions based on the user’s prompt and the function’s docstrings.
Here’s an agent with a tool that pulls information about a cat breed from thecatapi.com. You’ll need to install the requests library for this example:
(venv) $ python -m pip install requests
With requests installed, create a file called cats.py with the following code:
cats.py
import requests
from pydantic_ai import Agent
agent = Agent(
"google-gla:gemini-2.5-flash",
instructions="Help users with cat breeds. Be concise.",
)
@agent.tool_plain
def find_breed_info(breed_name: str) -> dict:
"""Find information about a cat breed."""
response = requests.get("https://api.thecatapi.com/v1/breeds")
response.raise_for_status()
json_response = response.json()
for breed in json_response:
if breed["name"] == breed_name:
return breed
return {"error": "Breed not found"}
result = agent.run_sync("Tell me about the Siamese cats.")
print(result.output)
When you run this script from your command line, you get a response based on the result of calling find_breed_info(). The output could look something like the following:
Siamese cats are known for being very vocal, active, and social.
They are affectionate and intelligent, often following their
owners around. They are adaptable and good with children and dogs.
Their origin is Thailand, and they typically live for 12-15 years.
The @agent.tool_plain decorator registers find_breed_info() as a tool the agent can invoke. The function takes a breed_name argument that the agent fills in based on the user’s prompt.
Note: You can also pass the tools to the Agent class during instantiation:
cats.py
# ...
def find_breed_info(breed_name: str) -> dict:
# ...
agent = Agent(
"google-gla:gemini-2.5-flash",
instructions="Help users with cat breeds. Be concise.",
tools=[find_breed_info]
)
The tools argument is a list of function objects. This is useful when you want to reuse tools across multiple agents. It can also give you more fine-grained control over the tools.
The docstring plays an important role here. The LLM reads "Find information about a cat breed." to understand what the function does. When a user asks about cat breeds, the agent recognizes it should call this function. Type hints on the parameters help the LLM pass the right data types.
When you ask "Tell me about the Siamese cats.", Pydantic AI sends your query to the LLM along with information about available tools. Then, the LLM decides that the agent should call find_breed_info() with breed_name="Siamese". Finally, the LLM receives the call result and generates a natural-language response.
You can register multiple tools on the same agent. The LLM will choose which tools to call based on the query and the docstrings. This lets you build agents that interact with databases, call external APIs, read files, and perform other operations.
Function calling helps you turn static LLMs into dynamic agents capable of performing real-world actions. Instead of just generating text, your agents can query systems, process data, run commands, search for real-time information, and more.
Inject Runtime Dependencies With Type Safety
Hardcoding database connections or API clients makes testing difficult, creates tight coupling, and reduces reusability. Dependency injection solves these issues by allowing you to pass a runtime context (RunContext) to your agents and tools. Pydantic AI provides a type-safe pattern for this.
Here’s an agent that uses dependency injection to access a simulated database:
users.py
import requests
from pydantic import BaseModel
from pydantic_ai import Agent, RunContext
class UserDatabase:
"""Simulate a user database using the JSONPlaceholder users API."""
_base_url = "https://jsonplaceholder.typicode.com"
def get_user_info(self, user_id: int) -> dict:
response = requests.get(f"{self._base_url}/users/{user_id}")
response.raise_for_status()
return response.json()
class UserSummary(BaseModel):
name: str
email: str
company: str
agent = Agent(
"google-gla:gemini-2.5-flash",
output_type=UserSummary,
deps_type=UserDatabase,
instructions=(
"You retrieve user information from an external database. "
"Use the available tools to gather user info, "
"then return a structured summary."
),
)
@agent.tool
def fetch_user(ctx: RunContext[UserDatabase], user_id: int) -> str:
"""Fetch user profile from the service."""
try:
user = ctx.deps.get_user_info(user_id)
return str(user)
except requests.HTTPError:
return f"User with ID {user_id} not found"
db = UserDatabase()
result = agent.run_sync(
"Get a summary for user 7",
deps=db # Inject the database
)
print(f"Name: {result.output.name}")
print(f"Email: {result.output.email}")
print(f"Company: {result.output.company}")
When you run this script, you’ll get the following output:
(venv) $ python users.py
Name: Kurtis Weissnat
Email: Telly.Hoeger@billy.biz
Company: Johns Group
In your code, the UserDatabase class is the dependency type. In this example, you use a regular Python class. However, dataclasses are generally convenient containers when your dependencies include multiple objects.
The deps_type argument to Agent specifies the type of dependency the agent should expect. Inside fetch_user(), you access this dependency via .deps on the runtime context. The ctx: RunContext[UserDatabase] type hint specifies the context type and the dependency type in square brackets. This gives you full type safety.
Note: Use @agent.tool when your tool needs access to the run context, like in fetch_user(), and @agent.tool_plain when it doesn’t.
Then, you create an instance of UserDatabase and pass it to the agent using the deps argument. This way, you inject the required database connection. Pydantic AI validates whether the dependency matches the expected type.
This separation between agent definition and runtime context makes testing straightforward, allowing you to inject a mock database for tests and a real database for production. For example, in your tests, you can override the dependency like this:
with agent.override(deps=TestUserDatabase()):
result = agent.run_sync("Get a summary for user 7")
You can inject any Python object as a dependency, including database connections, API clients, configuration objects, user sessions, and more.
Beware of Limitations and Gotchas
Pydantic AI provides powerful abstractions for creating AI-powered agents. However, you should understand the trade-offs before building production applications with this library.
-
Token costs add up: Each agent run consumes tokens, and costs can multiply quickly. Google Gemini’s free tier is good for experimentation, but you’ll hit limits with production traffic. In production, you need to watch out for:
- Input tokens: System prompts, instructions, tool definitions, and conversation history all count toward your input token budget. Long instructions or many tools increase costs per request.
- Output tokens: Structured outputs often require more tokens than simple strings. A detailed Pydantic model with many fields costs more than a one-sentence response.
- Tool-calling overhead: When an agent uses tools, it makes multiple round trips to the LLM. Each round trip adds latency and token consumption.
-
Validation retries increase costs and latency: When the LLM returns invalid data, Pydantic AI automatically retries the request. This improves reliability but has downsides:
- Latency: A retry doubles your response time. Multiple retries can make responses unacceptably slow.
- Cost: Each retry is another billable API call. If validation fails repeatedly, you pay for multiple attempts.
-
LLM features vary: Not all AI providers support structured outputs and tool calling equally in their LLMs. You’ll find the best support with models by OpenAI, Anthropic, and Google Gemini, which have robust structured output capabilities.
Pydantic AI can make agent development feel clean and Pythonic. However, as you move from experimentation to production, keep a close eye on both cost and latency. Also, make sure that your chosen model reliably supports structured outputs and tool calling.
Conclusion
You’ve learned how to set up Pydantic AI and build your first agent. You’ve returned structured, validated outputs by defining Pydantic models, enabled function calls by registering tools the LLM can invoke, and injected runtime dependencies with full type safety. You’ve also reviewed practical trade-offs around token costs, latency, and LLM features.
Pydantic AI saves you from error-prone string parsing on LLM responses by validating them against type-safe schemas. It helps you ship reliable LLM features with minimal boilerplate code.
In this tutorial, you’ve learned how to:
- Install and configure Pydantic AI with an AI provider and an API key
- Define Pydantic models to create AI-powered agents that produce structured outputs
- Register tools using the
@agent.tooland@agent.tool_plaindecorators for function calling - Use dependency injection for providing type-safe contexts to agents
- Weigh trade-offs in token costs, latency, and provider support
With these skills, you can start designing AI-powered agents that return structured data, call external tools, and remain testable and maintainable.
Next Steps
Now that you understand the core features of Pydantic AI, you can explore more advanced topics:
- Deepen your Pydantic knowledge: If you’re new to Pydantic models, Real Python’s tutorial on Pydantic: Simplifying Data Validation in Python covers schemas, field validators, and custom types that will strengthen the patterns you used in this tutorial.
- Dive into other Pydantic AI features: If you decide on Pydantic AI, you can explore other features, such as Model Context Protocol (MCP) support, multi-agent apps, agent-to-agent communication, and integration with Pydantic Logfire for monitoring and debugging.
You can also start creating fun projects, such as a customer service bot, a data analysis assistant, or a workflow automation agent. Pydantic AI’s type-safe architecture helps you ship reliable LLM applications faster. Real Python’s Python Coding With AI learning path brings together tutorials and video courses on LLM development, prompt engineering, and AI-assisted coding, so you can follow a structured curriculum as you keep building.
Get Your Code: Click here to download the free sample code you’ll use to work with Pydantic AI and build type-safe LLM agents in Python.
Frequently Asked Questions
Now that you have some experience with Pydantic AI in Python, you can use the questions and answers below to check your understanding and recap what you’ve learned.
These FAQs address the most important concepts you’ve covered in this tutorial. Click the Show/Hide toggle beside each question to reveal the answer.
Yes, Pydantic AI supports Ollama for local models and allows custom model adapters for any provider. However, if you’d like to use structured outputs and tool calling, make sure your models support these capabilities.
Basic familiarity helps but isn’t required. If you’ve used Python type hints and dataclasses, you can start immediately.
Yes, but consider API costs, rate limits, error handling, monitoring, and latency requirements. Pydantic AI supports async execution, streaming, retries, and other features that are important for production deployments.
Yes, agents maintain conversation context across turns. You can persist and restore conversations, though this tutorial focuses on single-shot interactions for simplicity.
Take the Quiz: Test your knowledge with our interactive “Pydantic AI: Build Type-Safe LLM Agents in Python” quiz. You’ll receive a score upon completion to help you track your learning progress:
Interactive Quiz
Pydantic AI: Build Type-Safe LLM Agents in PythonLearn the trade-offs of using Pydantic AI in production, including validation retries, structured outputs, tool usage, and token costs.


