Pydantic AI Tutorial: Building a Type-Safe LLM Agent from Scratch to Deployment

Anyone who has worked with LLM applications knows that the most painful part isn't integrating APIs, but rather dealing with the inconsistent output from models. Pydantic AI brings Pydantic's type validation to agent development, providing structured output and enabling static checks for tools. This article will guide you through the process, from installation to advanced topics, and share pitfalls I've encountered.

Introduction: Why Model Outputs Are Inconsistent

If you've worked with LLM APIs, you're probably familiar with this scenario: you ask the model to "return a JSON with name and score," and it works fine for the first nine attempts. However, on the tenth attempt, it adds a sentence like "Here's the result:" before the JSON, causing your json.loads() to fail. You then spend hours writing regular expressions to clean the string and if statements to check for fields, resulting in a significant portion of your "AI application" code being dedicated to battling the model's inconsistent output.

I once maintained an internal classification service, and dealing with the model occasionally missing a field took me three days to resolve. After switching to Pydantic AI, I was able to delete most of the defensive code, as the framework handled validation for me. This article will explain how to use it.

What is Pydantic AI

Pydantic AI is a Python Agent framework developed by the Pydantic team. You may have encountered the Pydantic name elsewhere, as it's used by official SDKs from OpenAI, Anthropic, and Google, as well as LangChain and LlamaIndex, for data validation. In other words, when it comes to validation, they are the most qualified people in the ecosystem.

Its design philosophy is similar to FastAPI: it uses Python's native type hints to define behavior clearly, leaving the rest to the framework. The core concepts are simple: Agent, Tools, Dependencies, and Structured Output. You don't need to memorize a bunch of custom abstract classes; writing code is just like writing regular Python, but with auto-completion and type checking (using tools like Pyright or mypy) that can catch errors before you even run the code.

It's model-agnostic, meaning it's not tied to a specific model vendor. With support for over a dozen vendors, including OpenAI, Anthropic, Google, Groq, Coherence, Mistral, and Ollama, switching models usually only requires changing a single string. To understand the difference between an Agent and a regular API call, you can start by reading what is an AI Agent.

Use Cases

In simple terms, Pydantic AI is suitable for any scenario where you need the model to return reliable results:

  • Structured extraction: Throw a customer complaint into the model and ask it to output sentiment, category, and urgency fields, ensuring the types are correct.
  • Classification and labeling: Labeling large documents with limited output, ensuring the model's response conforms to your defined Enum.
  • Tool-based Agents: Enable the model to call your functions, such as querying a database, calling a weather API, or performing mathematical calculations, with the framework handling the conversion of function types to tool descriptions that the model can understand.
  • RAG question-answering: Building a question-answering system with a vector database, which can be referenced in our RAG implementation guide.

Compared to large frameworks like LangChain, Pydantic AI is intentionally lightweight. If you only need to make model outputs more reliable without wanting to adopt an entire ecosystem for a small feature, its learning curve will be much friendlier.

Getting Started

1. Installation

bash
pip install pydantic-ai

It's recommended to create a virtual environment. Use Python 3.9 or later for better compatibility.

2. Setting API Keys

For example, with Anthropic, set an environment variable:

bash
export ANTHROPIC_API_KEY=your_key

For OpenAI, set OPENAI_API_KEY, and so on.

3. Writing Your First Agent

python
from pydantic_ai import Agent

agent = Agent('anthropic:claude-sonnet-4-6')
result = agent.run_sync('Explain what a vector database is in one sentence')
print(result.output)

The first parameter is the model name, in the format vendor:model. To switch to OpenAI, change it to 'openai:gpt-4o', and the rest of the code remains the same – this is the benefit of being model-agnostic.

4. Structuring Outputs

This is the key part. Define a Pydantic model as the output format:

python
from pydantic import BaseModel
from pydantic_ai import Agent

class Review(BaseModel):
sentiment: str # positive / negative / neutral
score: int # 1 to 5
summary: str

agent = Agent('anthropic:claude-sonnet-4-6', output_type=Review)
result = agent.run_sync('The food is delicious but I waited almost an hour, a bit exaggerated')
print(result.output.score) # Directly access the integer without parsing
print(result.output.sentiment) # Directly access the string

If the model's response doesn't match the Review type, the framework will automatically ask the model to retry. When you access result.output, it's already a validated Python object, and your IDE will provide auto-completion for fields.

5. Giving the Agent a Tool

python
from pydantic_ai import Agent

agent = Agent('anthropic:claude-sonnet-4-6')

@agent.tool_plain
def get_weather(city: str) -> str:
"""Query the current weather of a specified city"""
return f'{city} is currently 28 degrees, sunny'

result = agent.run_sync('What is the weather like in Taipei now?')
print(result.output)

The docstring isn't just for readability – it becomes the tool description that the model sees. The function's type hints (city: str) are also converted into parameter specifications that the model understands, with parameters undergoing Pydantic validation.

Advanced Tips

Dependency injection is one of its most underestimated features. You can pass data like database connections, user identities, or API clients into the Agent and tools in a type-safe manner using RunContext:

python
from dataclasses import dataclass
from pydantic_ai import Agent, RunContext

@dataclass
class Deps:
user_id: int
db: object # Your database connection

agent = Agent('anthropic:claude-sonnet-4-6', deps_type=Deps)

@agent.tool
def get_orders(ctx: RunContext[Deps]) -> str:
return f'Querying orders for user {ctx.deps.user_id}'

When writing tests, you can replace db with a mock object, avoiding the need to touch the real database, which is crucial for writing unit tests.

Streaming: For real-time typing effects, use agent.run_stream(), which validates and outputs structured data as it's generated, significantly improving user experience.

Observability: Pydantic AI integrates well with Logfire. Once connected, you can see every model call, every tool invocation, token consumption, and execution time. This makes debugging LLM applications much easier, as you no longer have to guess why the model responded in a certain way. For a more comprehensive Agent development guide, refer to our Agent development guide.

Common Errors and Precautions

  • Assuming output_type makes it 100% safe: The framework will retry validation failures, but there's a limit to retries. If the model continues to fail, it will throw an exception, which you still need to handle with try/except. Type validation reduces "dirty data" but doesn't guarantee the model will never make mistakes.
  • Writing vague docstrings for tools: The model relies on docstrings to decide when to call tools. Vague descriptions can lead to incorrect or missed tool calls. Treat docstrings as instructions for the model.
  • Putting too much logic in tools without handling exceptions: Errors in tool code will be returned to the model, potentially causing it to loop or waste tokens. Handle exceptions properly.
  • Ignoring costs: Structured output retries and multiple tool calls can consume tokens quickly. Always monitor costs before going live.
  • Using it as a large framework: Pydantic AI is intentionally lightweight. If you need complex multi-step orchestration or a suite of connectors, you might find LlamaIndex or another solution more convenient. Don't force it to be something it's not.

TheAI Academy Review

Honestly, the market is flooded with Agent frameworks, making it hard to choose. However, Pydantic AI addresses a specific pain point that every LLM developer faces: unreliable outputs. It doesn't aim to be the "strongest framework in the universe" but rather brings "type safety," a concept already valued in the Python community, into AI development cleanly. For those familiar with FastAPI and Pydantic, the learning curve is nearly nonexistent.

It won't make your model smarter, but it will make your code more reliable – and that's what truly saves you in the long run, especially when your application goes live and needs maintenance.

If you're just doing demos or casual experimentation, you might not appreciate its value deeply. However, once your project needs to go live, be used by real people, and require long-term maintenance, the importance of type safety and observability will become increasingly apparent.

References

Frequently Asked Questions

How does Pydantic AI differ from LangChain, and which one should I choose?

The main difference lies in their "weight". LangChain is a large ecosystem with numerous connectors, integrations, and abstraction layers, making it suitable for complex projects with steep learning curves. Pydantic AI, on the other hand, is intentionally lightweight, focusing on type safety with a core concept of agents, tools, dependency injection, and structured output. If your goal is to make model output more reliable and write code that's closer to native Python, Pydantic AI is easier to pick up. However, if you need extensive pre-built integrations, LangChain might be more convenient. Both options are not mutually exclusive, and the choice depends on the project's scale.

Do I have to use OpenAI's models, or can I integrate local models?

No, you don't have to use OpenAI's models. Pydantic AI is model-agnostic and supports over a dozen providers, including OpenAI, Anthropic, Google, Groq, Mistral, Cohere, and Ollama. Switching models usually only requires changing a single string when setting up the agent. To run local models, you can use Ollama by pointing the model string to your local service, and the rest of the code remains unchanged.

Can structured output really guarantee that the model won't return incorrect data?

While structured output can't guarantee that the model itself won't make mistakes, it ensures that "data that doesn't conform to your defined types won't sneak into the system". If the model returns data that fails Pydantic validation, the framework will automatically send an error message back to the model for retry. However, there's a limit to the number of retries, and persistent failures will raise an exception, so you should still handle the worst-case scenario using try/except. Structured output reduces the risk of dirty data, not the model's intelligence.

As a beginner with no prior experience with Pydantic, will it be difficult to learn?

If you have basic knowledge of Python and type hints, the barrier to entry is not high. Pydantic's core concept is defining data structures using classes, which is quite intuitive. I recommend spending 10 minutes learning how Pydantic defines BaseModel before diving into agent development, as it will make the process much smoother. The actual conceptual barrier lies in the design approach for agents and tools, which can be learned alongside our agent development guide.

繁體中文版 →