Pydantic AI & Instructor Basics
8 examples to get you started with structured, type-safe LLM output - 5 basic and 3 intermediate.
You will install the libraries, define Pydantic models, run a Pydantic AI agent with output_type, extract with Instructor, add a typed tool, and see validation retries.
Prerequisites
- Python 3.10+ recommended (3.11+ preferred)
- API keys for at least one model provider (OpenAI-compatible keys work for many paths)
python -m venv .venv && source .venv/bin/activate
pip install "pydantic-ai" instructor
export OPENAI_API_KEY="sk-..." # or provider-specific vars; verify at buildBasic Examples
1. Pydantic AI Hello World (Text Output)
Start with a minimal agent that returns plain text so you see the run API.
from pydantic_ai import Agent
agent = Agent(
"openai:gpt-4o-mini", # verify model slug at build
instructions="Be concise. Reply in one sentence.",
)
result = agent.run_sync('Where does "hello world" come from?')
print(result.output) # str
print(result.usage) # token / request usage; verify attribute at buildAgentowns model, instructions, tools, and output type.run_syncruns the agent loop until a final output is ready.- Default output type is text when you do not set
output_type.
Related: Why Structured Output Matters More as Agents Get Autonomous
2. Structured Final Output with output_type
Force the final answer into a validated Pydantic model.
from pydantic import BaseModel, Field
from pydantic_ai import Agent
class CityLocation(BaseModel):
city: str
country: str
year: int = Field(description="Year of the event asked about")
agent = Agent(
"openai:gpt-4o-mini", # verify at build
output_type=CityLocation,
instructions="Extract the city, country, and year from the question or known facts.",
)
result = agent.run_sync("Where were the Olympics held in 2012?")
print(result.output)
# CityLocation(city='London', country='United Kingdom', year=2012)
print(result.output.city) # typed attribute access- Pydantic builds the JSON schema the model must satisfy.
- Failed validation triggers reflection retries (subject to retry limits).
result.outputis typed asCityLocationfor IDE and type checkers.
3. Instructor One-Shot Extraction
Use Instructor when you need schema-constrained extraction without a full agent loop.
import instructor
from pydantic import BaseModel
class Person(BaseModel):
name: str
age: int
occupation: str
client = instructor.from_provider("openai/gpt-4o-mini") # verify slug at build
person = client.create(
response_model=Person,
messages=[
{
"role": "user",
"content": "Extract: Ada is a 36-year-old data engineer.",
}
],
)
print(person)
# Person(name='Ada', age=36, occupation='data engineer')response_modelis the Pydantic contract for the completion.- Instructor handles parse + validate + reask when validation fails.
- Same pattern works across many providers via
from_provider.
Related: Using Instructor for Schema-Constrained Outputs Across Providers
4. Field Descriptions Guide the Model
Add descriptions and constraints so the schema is self-documenting.
from enum import Enum
from pydantic import BaseModel, Field
from pydantic_ai import Agent
class Priority(str, Enum):
low = "low"
medium = "medium"
high = "high"
class TicketDraft(BaseModel):
title: str = Field(description="Short issue title, max ~80 chars")
priority: Priority
summary: str = Field(description="One paragraph for the human agent")
agent = Agent(
"openai:gpt-4o-mini",
output_type=TicketDraft,
instructions="Turn user complaints into support tickets.",
)
result = agent.run_sync("My card was charged twice and I need this fixed today.")
print(result.output.model_dump())- Enums prevent free-form priority strings.
- Field descriptions become part of the schema the model sees.
- Keep titles and summaries separate so UIs can bind fields cleanly.
5. Add a Plain Tool Alongside Structured Output
Combine a tool with a typed final output.
from pydantic import BaseModel
from pydantic_ai import Agent
class WeatherReport(BaseModel):
city: str
temp_c: float
advice: str
agent = Agent(
"openai:gpt-4o-mini",
output_type=WeatherReport,
instructions="Use the weather tool, then return a WeatherReport.",
)
@agent.tool_plain
def get_temp_c(city: str) -> float:
"""Return a demo temperature in Celsius for a city."""
# Replace with a real weather API in production.
return {"paris": 18.0, "tokyo": 24.5}.get(city.lower(), 20.0)
result = agent.run_sync("What is the weather like in Paris for a walk?")
print(result.output)@agent.tool_plainis for tools that do not needRunContext.- Tool return values go back to the model; final output still matches
WeatherReport. - Docstrings become the tool description for the model.
Intermediate Examples
6. Dependency Injection with RunContext
Pass typed deps (db handles, user ids) into tools safely.
from dataclasses import dataclass
from pydantic import BaseModel, Field
from pydantic_ai import Agent, RunContext
@dataclass
class SupportDeps:
customer_id: int
class SupportOutput(BaseModel):
advice: str
risk: int = Field(ge=0, le=10)
agent = Agent(
"openai:gpt-4o-mini",
deps_type=SupportDeps,
output_type=SupportOutput,
instructions="Give brief support advice and a risk score 0-10.",
)
@agent.tool
async def get_customer_id(ctx: RunContext[SupportDeps]) -> int:
"""Return the current customer id from dependencies."""
return ctx.deps.customer_id
result = agent.run_sync(
"I lost my card and saw a strange charge.",
deps=SupportDeps(customer_id=42),
)
print(result.output)deps_typemakes dependency injection type-safe.- Tools that need deps use
@agent.toolandRunContext[...]. - Tests can pass fake deps without rewriting tools.
7. Instructor Retries on Custom Validators
Fail validation on purpose so you see automatic reask.
import instructor
from pydantic import BaseModel, field_validator
class UserAge(BaseModel):
name: str
age: int
@field_validator("age")
@classmethod
def age_must_be_positive(cls, v: int) -> int:
if v < 0:
raise ValueError("age must be >= 0")
return v
client = instructor.from_provider("openai/gpt-4o-mini")
user = client.create(
response_model=UserAge,
max_retries=3,
messages=[
{
"role": "user",
"content": "Extract name and age. Text: Riley is -3 years old.",
}
],
)
print(user) # age corrected after retry feedback when the model cooperatesmax_retriescaps reask loops.- Validator error messages are fed back to the model.
- Prefer clear
ValueErrormessages; they become repair instructions.
Related: Handling Validation Failures and Retry Loops Gracefully
8. Choose the Right Tool for the Job
Sketch when to reach for each library in the same codebase.
# extraction.py - pure structured extract, no tools
import instructor
from pydantic import BaseModel
class InvoiceLine(BaseModel):
description: str
amount: float
class Invoice(BaseModel):
vendor: str
lines: list[InvoiceLine]
extract_client = instructor.from_provider("openai/gpt-4o-mini")
def extract_invoice(text: str) -> Invoice:
return extract_client.create(
response_model=Invoice,
messages=[{"role": "user", "content": text}],
max_retries=2,
)
# agent.py - multi-step tool use + structured finish
from pydantic_ai import Agent
triage_agent = Agent(
"openai:gpt-4o-mini",
output_type=Invoice,
instructions="If needed, call tools; finish with a validated Invoice.",
)- Instructor: thin, provider-flexible extraction layer.
- Pydantic AI: agent loop, tools, deps, streaming UI adapters.
- Sharing the same Pydantic models across both keeps contracts consistent.
What to Practice Next
- Design stricter schemas with nested models and enums.
- Stream Pydantic AI events into a Vercel AI SDK frontend.
- Cap retries and log validation failure rates in production.
Related: Pydantic AI's Native Vercel AI SDK Data Stream Support
Stack versions: Pins from the category manifest (verify at build): OpenRouter (~315+ models, July 2026 pricing/fees); LangGraph 1.0+; CrewAI 1.14+; Microsoft Agent Framework 1.0; Vercel AI SDK 6; Pydantic AI (latest); LlamaIndex (latest); OpenAI Agents SDK (latest + MCP); MCP (Linux Foundation governance); A2A (HTTP+SSE+JSON-RPC 2.0); Solana
@solana/web3.js+@solana/spl-token.