Typed LLM Structured Outputs with Instructor in Python (2026) — turn free-form model text into validated Pydantic objects, with automatic retries when the schema fails.
You already build agents with Pydantic AI and route models through LiteLLM. This tutorial fills the extraction gap: Instructor for one-shot (or few-retry) structured outputs without a full agent loop.
TL;DR
- Define a Pydantic
BaseModel, pass it asresponse_model - Prefer
instructor.from_provider("openai/…")for new code - Validation failures retry with feedback (
max_retries) - Works across providers; pair with LiteLLM when you need a gateway
Install
pip install instructor pydantic
# OpenAI is the default path; add extras for other providers:
# pip install "instructor[anthropic]"
# pip install "instructor[litellm]"
export OPENAI_API_KEY=sk-...
Example 1 — extract a typed object
import instructor
from pydantic import BaseModel, Field
class InvoiceLine(BaseModel):
description: str
amount_usd: float = Field(gt=0)
class Invoice(BaseModel):
vendor: str
total_usd: float = Field(gt=0)
lines: list[InvoiceLine]
client = instructor.from_provider("openai/gpt-4o-mini")
invoice = client.create(
response_model=Invoice,
messages=[
{
"role": "user",
"content": (
"Acme Supplies billed us $42.50 for 2 USB cables ($12.50 each) "
"and one HDMI adapter ($17.50)."
),
}
],
max_retries=3,
)
print(invoice.vendor, invoice.total_usd)
for line in invoice.lines:
print("-", line.description, line.amount_usd)
You get an Invoice instance, not a JSON string you have to parse by hand. If the model invents a negative amount, Pydantic rejects it and Instructor asks again.
Example 2 — validators that force cleaner data
from pydantic import BaseModel, Field, field_validator
import instructor
class Contact(BaseModel):
full_name: str
email: str
score: int = Field(ge=0, le=100)
@field_validator("email")
@classmethod
def must_look_like_email(cls, v: str) -> str:
if "@" not in v or "." not in v.split("@")[-1]:
raise ValueError("email must look like name@domain.tld")
return v.lower()
client = instructor.from_provider("openai/gpt-4o-mini")
contact = client.create(
response_model=Contact,
messages=[
{
"role": "user",
"content": "Lead: Jamie Rivera, jamie.rivera at example.com, interest score ninety.",
}
],
max_retries=2,
)
print(contact.model_dump())
When to use Instructor vs Pydantic AI
- Instructor — extract / classify / fill a schema from text or documents
- Pydantic AI — multi-step agents, tools, and typed deps (see the linked tutorial)
- LiteLLM — one client across providers; use
instructor[litellm]when the gateway already exists
For streaming tokens to a browser after you extract, see FastAPI SSE.
Production tips
- Keep secrets in env vars; never hardcode keys
- Pin model IDs; treat
response_modelas your API contract - Set
max_retriesdeliberately (2–3 is a good default) - Prefer nested models over free-form dicts for anything you store
- Older code using
from_openai/patchstill works — migrate towardfrom_providerfor new modules
Wrap-up
In 2026, structured LLM outputs should be typed. Instructor + Pydantic gives you validated objects, retries on schema failure, and a clear boundary between “chat text” and “data your app can trust.”