Instructor Guide 2025: Structured Outputs from LLMs with Pydantic
Instructor is the most popular Python library for getting LLMs to return validated, typed data instead of raw strings. One decorator patches your OpenAI, Anthropic, Groq, or Mistral client — after that you pass a Pydantic model and get a real Python object back, with automatic retry on validation failure.
What Is Instructor?
Instructor is an open-source Python library created by Jason Liu that wraps LLM provider clients and forces them to return structured, validated Pydantic objects. It works with OpenAI, Anthropic, Google Gemini, Mistral, Groq, Cohere, Ollama, and more. Under the hood Instructor translates your Pydantic model into whichever structured-output mechanism the provider supports — function calling, tool use, or JSON mode — and handles retries automatically when the model returns invalid data.
The core promise: instead of response.choices[0].message.content (a string you still have to parse), you get back a fully typed Python object that passed Pydantic validation.
Install
pip install instructor
Quickstart: Patch OpenAI and Get a Pydantic Object
Three steps: patch the client, define a BaseModel, pass it as response_model. The return value is an instance of your model — not a string, not JSON, a real Python object.
quickstart.py
import instructor
from openai import OpenAI
from pydantic import BaseModel
# 1. Patch the client
client = instructor.from_openai(OpenAI())
# 2. Define your schema
class UserInfo(BaseModel):
name: str
age: int
email: str
# 3. Call with response_model
user = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "user", "content": "Extract: John Doe, 34, [email protected]"}
],
response_model=UserInfo,
)
print(user.name) # "John Doe"
print(user.age) # 34
print(user.email) # "[email protected]"
print(type(user)) # <class '__main__.UserInfo'>
Instructor intercepts the response before it reaches your code, validates it against UserInfo, and returns the populated instance. If validation fails, it retries automatically.
Define Structured Schemas with Pydantic
Instructor passes your full Pydantic schema — including Field descriptions, nested models, validators, and Optional fields — to the model as its tool schema. Well-described fields dramatically improve extraction accuracy.
schemas.py — nested models, Field descriptions, validators
from pydantic import BaseModel, Field, field_validator
from typing import Optional, List
class Address(BaseModel):
street: str = Field(description="Street address including number")
city: str
country: str = Field(default="US")
class Person(BaseModel):
name: str = Field(description="Full legal name")
age: Optional[int] = Field(None, ge=0, le=150)
email: Optional[str] = None
addresses: List[Address] = Field(default_factory=list)
@field_validator("email")
@classmethod
def email_must_contain_at(cls, v):
if v and "@" not in v:
raise ValueError("Not a valid email address")
return v
# Instructor sends the full JSON schema to the model.
# Field descriptions become the model's "hints" for extraction. Multi-Provider Support
Switching providers is a one-line change. Instructor provides a from_* factory for every major provider. The rest of your code — response_model, retry logic, validators — stays identical.
providers.py — switch with one line
import instructor
import anthropic
import openai
from groq import Groq
from mistralai import Mistral
# OpenAI (function calling or JSON mode)
client = instructor.from_openai(openai.OpenAI())
# Anthropic Claude (tool use)
client = instructor.from_anthropic(anthropic.Anthropic())
# Groq (ultra-fast inference)
client = instructor.from_groq(Groq())
# Mistral (function calling)
client = instructor.from_mistral(Mistral(api_key="..."))
# Google Gemini
import google.generativeai as genai
client = instructor.from_gemini(genai.GenerativeModel("gemini-1.5-flash"))
# Ollama (local models)
client = instructor.from_openai(
openai.OpenAI(base_url="http://localhost:11434/v1", api_key="ollama"),
mode=instructor.Mode.JSON,
)
When calling Anthropic models, replace client.chat.completions.create() with client.messages.create() and add a max_tokens argument — everything else is the same.
Validation and Automatic Retry
When the model returns data that fails Pydantic validation, Instructor catches the ValidationError and sends it back to the model with an explanation, giving it a chance to self-correct. The max_retries parameter controls how many attempts are made before the error is raised.
retry.py — validation + max_retries
from pydantic import BaseModel, field_validator
import instructor
from openai import OpenAI
client = instructor.from_openai(OpenAI())
class PositiveNumber(BaseModel):
value: int
@field_validator("value")
@classmethod
def must_be_positive(cls, v):
if v <= 0:
raise ValueError(f"Value must be positive, got {v}")
return v
# Instructor will retry up to 3 times if the model returns
# a non-positive number, showing it the ValidationError each time.
result = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "Give me a positive number."}],
response_model=PositiveNumber,
max_retries=3, # default is 1
) Streaming Structured Outputs
For long responses, Instructor supports partial streaming via instructor.Partial[Model]. As tokens stream in, you get progressively more complete model instances — useful for showing incremental UI updates without waiting for the full response.
streaming.py — partial streaming
import instructor
from instructor import Partial
from openai import OpenAI
from pydantic import BaseModel
from typing import List
client = instructor.from_openai(OpenAI(), mode=instructor.Mode.TOOLS_STREAMING)
class Article(BaseModel):
title: str
summary: str
keywords: List[str]
# Stream partial Article objects as they arrive
for partial_article in client.chat.completions.create_partial(
model="gpt-4o",
messages=[{"role": "user", "content": "Summarize the history of Python."}],
response_model=Partial[Article],
):
# Fields are None until the model streams them
if partial_article.title:
print(f"Title so far: {partial_article.title}", end="\r")
# Final object is complete and fully validated
print(partial_article.model_dump()) Async Support with AsyncInstructor
For high-throughput pipelines, Instructor provides AsyncInstructor that works with asyncio. Use acreate() with asyncio.gather() to extract from many documents concurrently.
async_extraction.py — batch concurrent extraction
import asyncio
import instructor
from openai import AsyncOpenAI
from pydantic import BaseModel
# Use AsyncOpenAI + from_openai for async support
client = instructor.from_openai(AsyncOpenAI())
class Sentiment(BaseModel):
label: str # "positive" | "negative" | "neutral"
score: float # 0.0 to 1.0
async def classify(text: str) -> Sentiment:
return await client.chat.completions.acreate(
model="gpt-4o-mini",
messages=[{"role": "user", "content": f"Classify sentiment: {text}"}],
response_model=Sentiment,
)
async def main():
reviews = [
"This product is amazing!",
"Terrible experience, never again.",
"It was okay, nothing special.",
]
# Run all extractions concurrently
results = await asyncio.gather(*[classify(r) for r in reviews])
for review, result in zip(reviews, results):
print(f"{result.label} ({result.score:.2f}): {review}")
asyncio.run(main()) Instructor vs OpenAI Structured Outputs vs LangChain
Three common approaches to structured LLM outputs — each with different trade-offs:
| Feature | Instructor | OpenAI Structured Outputs | LangChain Parsers |
|---|---|---|---|
| Provider support | 10+ providers | OpenAI only | Many (via integrations) |
| Schema definition | Pydantic BaseModel | JSON Schema dict | Pydantic or custom |
| Auto validation retry | Yes (max_retries) | No (guaranteed format only) | Manual / OutputFixingParser |
| Partial streaming | Yes (Partial[Model]) | No | Limited |
| Async support | Yes (AsyncInstructor) | Yes | Yes |
| Setup complexity | Minimal (one patch line) | Minimal | High (chain setup) |
OpenAI Structured Outputs guarantees JSON format but does not validate semantic constraints — your validators will still fail silently. Instructor adds the retry loop. LangChain output parsers are powerful but carry significant framework overhead. For extraction pipelines that must be provider-agnostic, Instructor is the pragmatic choice.
Common Use Cases
Instructor shines any time you need to reliably parse unstructured text into typed data:
use_cases.py — extraction patterns
from pydantic import BaseModel, Field
from typing import List, Literal
# 1. Document data extraction
class Invoice(BaseModel):
vendor: str
total: float
currency: str = "USD"
line_items: List[str]
# 2. Zero-shot classification
class Category(BaseModel):
label: Literal["bug", "feature", "question", "spam"]
confidence: float = Field(ge=0, le=1)
# 3. Named entity recognition
class Entity(BaseModel):
text: str
type: Literal["PERSON", "ORG", "LOCATION", "DATE", "MONEY"]
start: int
end: int
class NERResult(BaseModel):
entities: List[Entity]
# 4. Agentic pipeline step
class ActionPlan(BaseModel):
reasoning: str = Field(description="Chain-of-thought reasoning")
next_action: Literal["search", "summarize", "ask_user", "done"]
action_input: str These patterns are directly composable into agentic pipelines — each step produces a typed object that the next step consumes, eliminating JSON parsing and ad-hoc string manipulation throughout the chain.
Instructor Routes Through OpenAI, Anthropic, and Groq
Instructor routes through OpenAI, Anthropic, and Groq. When those APIs go down, your extraction pipeline breaks. Prismix monitors all of them.
Monitor LLM API Status →