HomeAboutOur TeamContact
HomeArtificial Intelligence
Pydantic AI Tutorial: Type-Safe AI Agents in Python (2026)

Pydantic AI Tutorial: Type-Safe AI Agents in Python (2026)

Artificial Intelligence
June 15, 2026
6 min read
Intermediate
Pydantic AI tutorial title beside a Python code card showing a typed BaseModel and a validated result.output with a green check
Table of Contents
01
What We're Building With Pydantic AI
02
Prerequisites and Your First Typed Agent
03
Step 1 — Return Structured Data You Can Trust
04
Step 2 — Give the Agent a Tool
05
Step 3 — Connect a Pydantic AI Agent to an MCP Server
06
Step 4 — Testing It and the Errors I Hit
07
When to Reach for Something Else — and What to Build Next
08
Frequently Asked Questions
09
Conclusion

Everyone reaches for LangChain when they build their first agent. I did too. Then I spent one too many evenings fighting untyped dictionary soup. The model returns a string when I expected a number, and the pipeline falls over three functions later. So I tried Pydantic AI instead, and a whole class of “the model returned garbage” bugs simply disappeared.

This Pydantic AI tutorial builds a type-safe AI agent in Python from scratch. The agent returns a validated object, calls a Python tool, and talks to a remote MCP server. By the end you’ll have runnable code. You’ll also know when this framework is the right call, and when it isn’t.

Pydantic AI comes from the team behind Pydantic, the validation library that already sits under FastAPI. It crossed 17,000 GitHub stars in 2026. It treats type safety as the whole point, not an afterthought. Let’s build something with it.

What We’re Building With Pydantic AI

We’re building a small agent that answers a question and hands back a typed Python object instead of a blob of text. Your IDE autocompletes the fields. Your type checker catches mistakes before they ship.

Pydantic AI agent architecture: a prompt goes to the agent, the LLM calls a typed tool and returns JSON, which Pydantic validates into a typed Python object

The diagram shows the core loop. Your prompt goes in. The language model does its work, calling a tool if it needs one. Whatever comes back is validated against a Pydantic model before your code touches it. If the shape is wrong, the agent retries. You never parse a raw string by hand again.

This guide suits an intermediate Python developer. You don’t need prior agent experience, but you should be comfortable with classes and type hints.

Prerequisites and Your First Typed Agent

Before the interesting parts, here’s the short list of what you need to follow along.

  • Python 3.9+ and a virtual environment
  • A model provider key — I use Google Gemini because its free tier is plenty for learning and works fine from India
  • Ten minutes and a terminal

Install the library and set your key:

bash
python -m pip install pydantic-ai
export GOOGLE_API_KEY="your-key-here"

Now the smallest agent that shows the point of the whole framework. Define a Pydantic model. Pass it as output_type. Then ask a question:

python
from pydantic import BaseModel
from pydantic_ai import Agent
class CityInfo(BaseModel):
name: str
country: str
population: int
agent = Agent(
"google-gla:gemini-2.5-flash",
output_type=CityInfo,
instructions="Return facts about the city the user names.",
)
result = agent.run_sync("Tell me about Bengaluru")
print(result.output) # CityInfo(name='Bengaluru', country='India', ...)
print(result.output.population) # an int — not the string "13000000"

That output_type=CityInfo line is the entire idea. Pydantic AI turns your model into a JSON schema. It tells the LLM to match that schema. Then it validates the reply and returns a real CityInfo object. So result.output.population is an int your editor knows about.

Six-step flow for building a type-safe Pydantic AI agent: install, define output model, create the agent, add a tool, connect an MCP toolset, then run and get a typed result

One small note on that instructions argument. Pydantic AI splits the old “system prompt” into instructions and system_prompt. Use instructions for a single agent, which is what we want here. If you’ve never built an agent before, read what AI agents actually are first, then come back.

Step 1 — Return Structured Data You Can Trust

The early win is simple: you stop writing parsing code. The first time I returned a CityInfo object and read .population as a number, I deleted about thirty lines of regex and try/except. That code used to clean up messy model output. Now I don’t need it.

Behind the scenes, Pydantic AI validates every response. Say the model returns a missing field, or a string where an int belongs. Pydantic raises a validation error. The agent then retries the request automatically. You can tune how many times with output_retries:

python
agent = Agent(
"google-gla:gemini-2.5-flash",
output_type=CityInfo,
output_retries=2, # retry validation failures up to twice
)

You can push validation further with Field constraints. Mark a value as positive, and a bad number from the model gets rejected before it reaches you:

python
from pydantic import BaseModel, Field
class CityInfo(BaseModel):
name: str
country: str
population: int = Field(gt=0) # must be greater than zero

Now a negative or zero population fails validation, and the agent is told to try again. The rule lives in one place — the model — not scattered through if checks across your codebase. It’s the same Field you already use in Pydantic and FastAPI, so there’s nothing new to learn.

That reliability isn’t free, and I’ll come back to the cost. But for any case where model output feeds other code, a guaranteed shape beats a hopeful string. This is the single reason I keep choosing it.

Step 2 — Give the Agent a Tool

An agent that only knows what’s in the model’s head is limited. Tools let it reach out, whether to a database, an API, or the filesystem. In Pydantic AI you register a tool with a decorator. The function’s type hints and docstring become the contract the model reads.

This is where most people get stuck, so read this twice. If you forget the type hints on your tool’s parameters, the model can’t build a reliable schema, and tool calls get flaky. Here’s a tool that looks a user up over HTTP. It injects a dependency through RunContext:

python
import httpx
from pydantic import BaseModel
from pydantic_ai import Agent, RunContext
class UserSummary(BaseModel):
name: str
email: str
company: str
agent = Agent(
"google-gla:gemini-2.5-flash",
deps_type=httpx.Client,
output_type=UserSummary,
instructions="Look up the user by ID, then return a short summary.",
)
@agent.tool
def fetch_user(ctx: RunContext[httpx.Client], user_id: int) -> dict:
"""Fetch a user profile by numeric ID."""
resp = ctx.deps.get(f"https://jsonplaceholder.typicode.com/users/{user_id}")
resp.raise_for_status()
return resp.json()
with httpx.Client() as client:
result = agent.run_sync("Summarise user 7", deps=client)
print(result.output.company)

The deps_type plus RunContext[httpx.Client] pattern is dependency injection. You pass in things like clients or connections at runtime, instead of hardcoding them. It keeps the tool testable. You swap a fake client in tests and a real one in production.

Common mistake: using @agent.tool for a function that never touches ctx. If your tool doesn’t need the run context, use @agent.tool_plain instead. It drops the RunContext argument and reads cleaner.

Step 3 — Connect a Pydantic AI Agent to an MCP Server

Here’s the part that makes Pydantic AI feel current in 2026. It speaks MCP (Model Context Protocol — an open standard that lets agents call external tools through one interface) natively. Instead of writing every tool yourself, you point the agent at an MCP server. Its tools then show up automatically.

Each server is a toolset you pass to the agent. Connecting to one over streamable-HTTP takes three lines:

python
from pydantic_ai import Agent
from pydantic_ai.mcp import MCPServerStreamableHTTP
server = MCPServerStreamableHTTP("http://localhost:8000/mcp")
agent = Agent("google-gla:gemini-2.5-flash", toolsets=[server])
async def main():
async with agent: # opens/closes the connection around the run
result = await agent.run("Add 7 and 5 using your tools")
print(result.output)

The async with agent block opens connections to every registered server, runs your agent, then cleans up. Note the transport in the URL. For local experiments you can run a server over stdio, but a deployed agent needs HTTP. Streamable-HTTP is the transport you want here, and it’s what most 2026 servers expose.

Say you’ve already built a server, maybe from my guide on building a production-ready MCP server in Python. This is exactly how an agent consumes it. One standard, and you can swap Gemini for GPT or Claude underneath without touching the tool layer. That portability is the real reason MCP took over this year.

Step 4 — Testing It and the Errors I Hit

You don’t want a live LLM call in your unit tests. It’s slow, it costs money, and it gives different answers each run. Pydantic AI ships a TestModel that returns canned, schema-valid data. So you can test your wiring offline:

python
from pydantic_ai.models.test import TestModel
with agent.override(model=TestModel()):
result = agent.run_sync("Summarise user 7", deps=client)
# assert against result.output here — no API call made

A few errors I actually ran into while writing this:

  • UserError: no API key I forgot to export GOOGLE_API_KEY in the new shell. Keys don’t carry across terminal sessions.
  • Flaky tool calls — caused by a tool parameter with no type hint. Add the hint and the schema firms up.
  • await missing — agent.run() is async. In a plain script use agent.run_sync(), or wrap the async version in asyncio.run(main()).

When to Reach for Something Else — and What to Build Next

Pydantic AI isn’t always the answer, and pretending otherwise would waste your time. Say you need a huge ecosystem of prebuilt integrations: dozens of vector stores, retrievers, document loaders. LangChain or LlamaIndex still ship more off-the-shelf parts. There’s also a real cost to the safety. Every validation retry is another billable API call, and it roughly doubles your latency. So a model that keeps missing your schema gets expensive fast. For a side-by-side on orchestration frameworks, see my LangGraph vs CrewAI vs AutoGen comparison.

That said, for most single-agent apps where you care about clean output, I prefer Pydantic AI. The type safety pays for itself the first time a model hands you nonsense and the framework catches it, instead of your users. If you came from Java like I did, the near-compile-time guarantees feel like home.

Next, I’d extend this agent with persistent memory so it remembers past turns. That’s a natural follow-on to the agentic AI app series.

Frequently Asked Questions

What is Pydantic AI? It’s a Python framework from the Pydantic team for building type-safe LLM agents. You define a Pydantic model, pass it as the agent’s output_type, and get back a validated object instead of a raw string. It supports OpenAI, Anthropic, Gemini, Ollama, and more.

Is Pydantic AI better than LangChain? It depends on the job. I reach for Pydantic AI when I want clean, type-safe output and little boilerplate in a single-agent app. LangChain and LlamaIndex still win when you need a big ecosystem of prebuilt integrations like vector stores and retrievers.

Does Pydantic AI support MCP? Yes, natively. Each MCP server is a toolset — create a client like MCPServerStreamableHTTP and pass it to the agent via toolsets=[...], and its tools show up automatically.

Can I use Pydantic AI with a free model? Yes. Google Gemini’s free tier is plenty for learning and works fine from India. You can also run local models through Ollama with no API key.

How does output_type make an agent type-safe? Pydantic AI turns your model into a JSON schema, tells the LLM to match it, and validates the reply. If the shape is wrong, the agent retries — so result.output is always a valid, typed object.

Conclusion

You’ve built a type-safe agent that returns a validated object. It calls a tool with injected dependencies. It talks to an MCP server. And it runs under tests without hitting a real model. That’s a production-shaped foundation, not a toy.

The obvious next step is to give this agent real tools to call. Read next: Build a Production-Ready MCP Server in Python. Write the server, then point this Pydantic AI agent at it.

What’s the first tool you’d hand a type-safe agent — a database lookup, an internal API, or something else? Tell me in the comments.


References

  1. Pydantic AI — official documentation
  2. pydantic/pydantic-ai on GitHub
  3. Pydantic AI — MCP client docs
  4. Real Python — Pydantic AI: Build Type-Safe LLM Agents
  5. Model Context Protocol — specification

Tags

#PydanticAI#PythonTutorial#AIAgents#TypeSafety#AgentFramework#ModelContextProtocol#AIForDevelopers

Share

Previous Article
What Is an MCP Server? Complete Guide for Developers (2026)
Sukhveer Kaur
More from this author

Sukhveer Kaur

Agentic AI roadmap 2026 banner showing a five-stage learning path with milestones from Python to LLM basics, a first agent, tools and MCP, and deploy
Agentic AI Roadmap 2026: Worth It + the Exact Path
June 16, 2026
5 min
Beginner
See all by Sukhveer Kaur

Get new guides in your inbox

Practical AI, software engineering, and cloud articles — straight to your inbox. No spam, unsubscribe anytime.
Pydantic AI Tutorial: Type-Safe AI Agents in Python (2026)
6 min left
Sukhveer Kaur

Sukhveer Kaur

Software Developer & AI Engineer

Popular Posts

01
Agentic AI Roadmap 2026: Worth It + the Exact Path
Artificial Intelligence
·
5 min read

Table Of Contents

1
What We're Building With Pydantic AI
2
Prerequisites and Your First Typed Agent
3
Step 1 — Return Structured Data You Can Trust
4
Step 2 — Give the Agent a Tool
5
Step 3 — Connect a Pydantic AI Agent to an MCP Server
6
Step 4 — Testing It and the Errors I Hit
7
When to Reach for Something Else — and What to Build Next
8
Frequently Asked Questions
9
Conclusion

Related Posts

© 2026, All Rights Reserved.

Quick Links

Advertise with usOur TeamAbout UsEditorial StandardsContact Us

Social Media