Crafting Type-Safe LLM Agents: A Step-by-Step Guide with Pydantic AI
Introduction
If you've ever wrestled with raw, unstructured text responses from large language models (LLMs), you know the frustration of trying to extract reliable data. Pydantic AI offers a Pythonic solution by letting you build agents that return validated, structured outputs using familiar Pydantic models. Instead of parsing unpredictable strings, you get type-safe objects with automatic validation—a pattern that will feel instantly comfortable if you've used FastAPI or Pydantic before. In this guide, you'll learn to construct such agents step by step, from defining schemas to handling retries and selecting the best LLM provider.

What You Need
- Python 3.9+ installed on your system
- Pydantic AI library (
pip install pydantic-ai) - An API key for at least one supported LLM provider: Google Gemini, OpenAI, or Anthropic
- Basic familiarity with Python type hints and Pydantic models
- A code editor of your choice
Step 1: Define Your Structured Output Model
Start by creating a BaseModel subclass that specifies exactly what data you want your LLM agent to return. This model acts as your schema, guaranteeing type safety and automatic validation.
from pydantic import BaseModel
class StockInfo(BaseModel):
symbol: str
price: float
change_percent: float
Every field must be annotated with a Python type. Pydantic AI will enforce that the LLM's output conforms to this structure. If the model returns malformed data (e.g., a string where a float is expected), the agent can automatically retry (see Step 4).
Step 2: Register Tools with the @agent.tool Decorator
Tools are Python functions that your LLM agent can invoke to perform actions—like looking up stock prices or fetching weather data. Register them using the @agent.tool decorator:
from pydantic_ai import Agent
agent = Agent(model='gemini-1.5-pro', result_type=StockInfo)
@agent.tool
def get_stock_price(ctx, symbol: str) -> dict:
"""Retrieve the current price and change for a given stock symbol."""
# Your API call or database query goes here
return {'price': 150.25, 'change_percent': 1.34}
The decorator makes the function callable by the LLM. The docstring is crucial: it describes the tool's purpose and helps the LLM decide when to use it. The first argument ctx provides context about the conversation.
Step 3: Inject Dependencies with deps_type
Instead of relying on global state (which can cause issues in production), Pydantic AI supports dependency injection. Define a dependency class and pass it via deps_type:
class MyDeps(BaseModel):
db_connection: str
api_key: str
agent = Agent(
model='gemini-1.5-pro',
result_type=StockInfo,
deps_type=MyDeps
)
@agent.tool
def get_stock_price(ctx, symbol: str) -> dict:
# Use ctx.deps.db_connection and ctx.deps.api_key
...
This keeps your code clean, testable, and thread‑safe. When running the agent, you provide the actual dependencies:
deps = MyDeps(db_connection='sqlite:///stocks.db', api_key='sk-12345')
result = agent.run('What is the price of AAPL?', deps=deps)
Step 4: Enable Validation Retries for Reliability
LLMs sometimes return outputs that don't match your schema—missing fields or wrong types. Pydantic AI can automatically retry the query when validation fails:

agent = Agent(
model='gemini-1.5-pro',
result_type=StockInfo,
retries=3 # default is 1
)
Each retry sends a new request to the LLM with details about the validation error, giving it a chance to correct its output. While this increases reliability, be aware that it also increases API costs. Adjust the retries parameter based on your tolerance for failures and budget.
Step 5: Choose the Right LLM Provider
Not all LLMs handle structured output equally well. For the best results with Pydantic AI, use one of these providers:
- Google Gemini – Excellent support for structured outputs natively.
- OpenAI – Strong performance with function calling and response_format.
- Anthropic – Great for complex instructions and consistent formatting.
Other providers (local models, older APIs) may have limited or no structured output capabilities. Always test your agent with the chosen model to ensure it can reliably produce the desired schema.
Tips for Success
- Write clear docstrings for your tools. The LLM uses them to understand what each tool does. Include examples of expected inputs and outputs when possible.
- Start with simple models. If your schema is too complex, the LLM may struggle. Gradually add fields as you verify correctness.
- Monitor your API costs. Each retry uses another request. Use the minimum number of retries that still gives acceptable reliability (start with 2).
- Use environment variables for API keys. Never hard‑code credentials into your dependency definitions.
- Test with varied queries. Validate that your agent handles edge cases—like unknown stock symbols or malformed queries—gracefully.
- Keep your dependency injection lightweight. Only pass what the tools actually need to avoid overhead.
Related Articles
- From Reading to Mastery: 7 Essential Steps to Truly Understand Algorithms
- Python 3.15 Alpha 5 Released: Key Features and Performance Gains
- IntelliJ IDEA Mastery Series Launches: Developer Productivity Secrets Revealed
- Everything You Need to Know About Python 3.15.0 Alpha 3
- Understanding Type Construction and Cycle Detection in Go's Type Checker
- Kubernetes v1.36 Breaks Cycle of Policy Insecurity with Startup-Only Admission Controls
- New AI Plugin 'Destiny' Brings Ancient East Asian Astrology to Claude Code
- The Unseen Dependencies: How TCMalloc Challenged Kernel's API Stability