Instructor: Getting Structured Output from LLMs with Python
One of the biggest challenges when working with Large Language Models (LLMs) is getting structured and consistent output. LLMs by default produce free-form text, which is difficult to parse and integrate into applications. The Instructor library solves this problem by leveraging Pydantic for validation and structured data extraction from LLMs.
In this tutorial, we will learn how to use Instructor to get reliable JSON/Pydantic outputs from various LLM providers such as OpenAI, Anthropic, and others.
What Is Instructor?
Instructor is a Python library that patches LLM clients (like OpenAI) to return validated Pydantic objects instead of plain strings. Instructor works by leveraging function calling or JSON mode from the LLM, then validates the results using Pydantic.
Key advantages of Instructor:
- Type-safe: Output is guaranteed to match the defined Pydantic schema
- Automatic retry: If validation fails, Instructor automatically retries with error feedback
- Streaming support: Supports partial streaming for complex objects
- Multi-provider: Supports OpenAI, Anthropic, Google, Mistral, and more
- Custom validation: You can add Pydantic validators for business logic
Installation
First, install Instructor along with the required dependencies:
pip install instructor openai pydantic
For other providers, install additional dependencies:
# For Anthropic
pip install instructor anthropic
For Google Gemini
pip install instructor google-generativeai
For Mistral
pip install instructor mistralai
Make sure you have an API key from the provider you will be using:
export OPENAIAPIKEY="sk-your-api-key-here"
Basic Usage with OpenAI
Let's start with a simple example: extracting user information from text.
import instructor
from openai import OpenAI
from pydantic import BaseModel
Patch OpenAI client with Instructor
client = instructor.fromopenai(OpenAI())
Define the output schema
class UserInfo(BaseModel):
name: str
age: int
email: str
Extract structured data from text
user = client.chat.completions.create(
model="gpt-4o-mini",
responsemodel=UserInfo,
messages=[
{
"role": "user",
"content": "My name is John Smith, I'm 28 years old. "
"My email is john.smith@email.com"
}
],
)
print(user)
UserInfo(name='John Smith', age=28, email='john.smith@email.com')
print(user.name) # John Smith
print(user.age) # 28
print(user.email) # john.smith@email.com
Notice that responsemodel=UserInfo is the key parameter that tells Instructor what schema to expect. The result is not a dictionary or string, but a validated Pydantic object.
Complex Pydantic Models
Instructor supports complex Pydantic models including nested models, optional fields, enums, and lists.
from pydantic import BaseModel, Field
from typing import Optional, List
from enum import Enum
class JobLevel(str, Enum):
JUNIOR = "junior"
MID = "mid"
SENIOR = "senior"
LEAD = "lead"
class Skill(BaseModel):
name: str = Field(description="Name of the skill or technology")
yearsexperience: int = Field(
description="Years of experience", ge=0, le=50
)
proficiency: str = Field(
description="Proficiency level: beginner, intermediate, advanced"
)
class WorkExperience(BaseModel):
company: str
role: str
durationmonths: int = Field(ge=1)
description: str
class CandidateProfile(BaseModel):
name: str
currentrole: str
level: JobLevel
totalyearsexperience: int = Field(ge=0)
skills: List[Skill]
workhistory: List[WorkExperience]
education: str
summary: str = Field(
description="Profile summary of the candidate in 2-3 sentences"
)
resumetext = """
I'm Sarah Chen, currently working as a Senior Data Engineer at Spotify
for the past 3 years. Previously I worked at Netflix as a Data Engineer
for 2 years. I have 5 years of experience in data engineering with main
expertise in Python (5 years, advanced), Apache Spark (4 years, advanced),
and SQL (5 years, advanced). I'm also familiar with Kafka (2 years,
intermediate) and Kubernetes (1 year, beginner).
BS in Computer Science from Stanford University.
"""
profile = client.chat.completions.create(
model="gpt-4o",
responsemodel=CandidateProfile,
messages=[
{
"role": "system",
"content": "Extract the candidate profile from the following resume text."
},
{"role": "user", "content": resumetext}
],
)
print(f"Name: {profile.name}")
print(f"Level: {profile.level.value}")
print(f"Skills:")
for skill in profile.skills:
print(f" - {skill.name}: {skill.yearsexperience} years "
f"({skill.proficiency})")
Validation with Pydantic Validators
One of the most powerful features of Instructor is the ability to add custom validation. If validation fails, Instructor will automatically retry and send the error message to the LLM.
from pydantic import BaseModel, Field, fieldvalidator, modelvalidator
class SQLQuery(BaseModel):
query: str = Field(description="A valid SQL query")
explanation: str = Field(description="Explanation of the query")
tables
used: List[str] = Field(description="List of tables used")
@fieldvalidator("query")
@classmethod
def validatenodelete(cls, v: str) -> str:
if "DELETE" in v.upper() or "DROP" in v.upper():
raise ValueError(
"Query must not contain DELETE or DROP for data safety"
)
return v
@fieldvalidator("query")
@classmethod
def validatehasselect(cls, v: str) -> str:
if not v.strip().upper().startswith("SELECT"):
raise ValueError("Query must start with SELECT")
return v
@modelvalidator(mode="after")
def validatetablesmatchquery(self):
for table in self.tablesused:
if table.lower() not in self.query.lower():
raise ValueError(
f"Table '{table}' not found in query"
)
return self
result = client.chat.completions.create(
model="gpt-4o-mini",
responsemodel=SQLQuery,
messages=[
{
"role": "user",
"content": "Create a query to show the top 10 customers "
"with the highest total purchases from the "
"customers and orders tables"
}
],
)
print(f"Query: {result.query}")
print(f"Explanation: {result.explanation}")
print(f"Tables: {result.tablesused}")
Retry Logic and Error Handling
Instructor has a built-in retry mechanism that is very useful when the LLM produces invalid output.
from instructor import retry
from tenacity import retry, stopafterattempt, waitfixed
Configure retry when creating the client
client = instructor.fromopenai(
OpenAI(),
maxretries=3, # Maximum 3 retries
)
Or configure retry per-request
class StrictOutput(BaseModel):
sentiment: str = Field(
description="Sentiment: positive, negative, or neutral"
)
confidence: float = Field(ge=0.0, le=1.0)
keyphrases: List[str] = Field(minlength=1, maxlength=5)
@fieldvalidator("sentiment")
@classmethod
def validatesentiment(cls, v: str) -> str:
allowed = {"positive", "negative", "neutral"}
if v.lower() not in allowed:
raise ValueError(
f"Sentiment must be one of: {allowed}"
)
return v.lower()
result = client.chat.completions.create(
model="gpt-4o-mini",
responsemodel=StrictOutput,
maxretries=5,
messages=[
{
"role": "user",
"content": "Analyze sentiment: 'This product is amazing, "
"fast delivery, but the packaging could be better'"
}
],
)
print(f"Sentiment: {result.sentiment}")
print(f"Confidence: {result.confidence}")
print(f"Key phrases: {result.keyphrases}")
When validation fails, Instructor automatically:
Streaming with Partial Objects
For large objects, Instructor supports streaming so you can receive data incrementally.
from instructor import Partial
class Article(BaseModel):
title: str
summary: str
keypoints: List[str]
conclusion: str
Streaming with Partial
stream = client.chat.completions.create(
model="gpt-4o-mini",
responsemodel=Partial[Article],
stream=True,
messages=[
{
"role": "user",
"content": "Write a short article about the benefits of AI "
"in healthcare"
}
],
)
for partialarticle in stream:
# Each iteration gets a partially filled Article object
if partialarticle.title:
print(f"Title: {partialarticle.title}")
if partialarticle.keypoints:
print(f"Points so far: {len(partialarticle.keypoints)}")
Practical Example: Data Extraction from Text
Here is a real-world example for extracting product information from e-commerce descriptions.
from pydantic import BaseModel, Field
from typing import List, Optional
class ProductSpec(BaseModel):
key: str = Field(description="Specification name")
value: str = Field(description="Specification value")
class ExtractedProduct(BaseModel):
name: str = Field(description="Product name")
brand: str = Field(description="Product brand")
priceusd: Optional[float] = Field(
description="Price in USD", default=None
)
category: str = Field(description="Product category")
specifications: List[ProductSpec] = Field(
description="Technical specifications"
)
pros: List[str] = Field(description="Product advantages")
cons: List[str] = Field(description="Product disadvantages")
productdescription = """
Samsung Galaxy S24 Ultra Review - Price $1,299
Samsung delivers its best flagship yet. The Galaxy S24 Ultra features a
6.8-inch Dynamic AMOLED 2X display, Snapdragon 8 Gen 3 processor,
12GB RAM, and 256GB storage. The 200MP main camera produces incredibly
detailed photos. The 5000mAh battery lasts all day.
The pros are blazing fast performance, incredible camera, and the
useful S Pen. The cons are the expensive price tag and the relatively
heavy weight at 233 grams.
"""
product = client.chat.completions.create(
model="gpt-4o",
responsemodel=ExtractedProduct,
messages=[
{
"role": "system",
"content": "Extract product information from the following review."
},
{"role": "user", "content": productdescription}
],
)
print(f"Product: {product.brand} {product.name}")
print(f"Price: ${product.priceusd:,.2f}")
print(f"Category: {product.category}")
print("Specifications:")
for spec in product.specifications:
print(f" {spec.key}: {spec.value}")
Practical Example: Text Classification
Instructor is perfect for classification tasks because the output is guaranteed to match the defined categories.
from enum import Enum
from typing import List
class TicketPriority(str, Enum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
CRITICAL = "critical"
class TicketCategory(str, Enum):
BUG = "bug"
FEATUREREQUEST = "featurerequest"
QUESTION = "question"
COMPLAINT = "complaint"
BILLING = "billing"
class ClassifiedTicket(BaseModel):
priority: TicketPriority
category: TicketCategory
department: str = Field(
description="Department responsible for handling"
)
summary: str = Field(
description="One-sentence ticket summary", maxlength=200
)
suggestedresponse: str = Field(
description="Suggested initial response for customer service"
)
requiresescalation: bool = Field(
description="Whether escalation to a higher level is needed"
)
tickettext = """
URGENT: Payment system has been down since yesterday. All transactions
are failing and customers cannot checkout. We've lost approximately
$50,000 in potential revenue. Please fix immediately!
"""
classified = client.chat.completions.create(
model="gpt-4o-mini",
responsemodel=ClassifiedTicket,
messages=[
{
"role": "system",
"content": "Classify the following support ticket."
},
{"role": "user", "content": tickettext}
],
)
print(f"Priority: {classified.priority.value}")
print(f"Category: {classified.category.value}")
print(f"Department: {classified.department}")
print(f"Escalation needed: {classified.requiresescalation}")
print(f"Summary: {classified.summary}")
Using with Other Providers
Instructor is not limited to OpenAI. Here is how to use it with Anthropic.
import instructor
from anthropic import Anthropic
With Anthropic Claude
client = instructor.fromanthropic(Anthropic())
class Analysis(BaseModel):
topic: str
mainarguments: List[str]
conclusion: str
result = client.messages.create(
model="claude-sonnet-4-20250514",
maxtokens=1024,
responsemodel=Analysis,
messages=[
{
"role": "user",
"content": "Analyze the main arguments about the importance "
"of AI regulation in Southeast Asia"
}
],
)
Tips and Best Practices
Here are some tips to maximize your use of Instructor:
1. Use clear Field descriptionsclass GoodModel(BaseModel):
# Good - clear description
revenue: float = Field(
description="Total revenue in USD millions"
)
# Not ideal - no description
revenue: float
2. Choose the right model for the right task
- Use
gpt-4o-minifor simple tasks (classification, basic extraction) - Use
gpt-4ofor complex tasks (nested objects, reasoning)
messages=[
{
"role": "system",
"content": "You are a data extraction assistant. "
"Always provide output in English. "
"If information is not available, use null."
},
{"role": "user", "content": text}
]
4. Handle errors gracefully
from instructor.exceptions import InstructorRetryException
try:
result = client.chat.completions.create(
model="gpt-4o-mini",
responsemodel=MyModel,
max_retries=3,
messages=[{"role": "user", "content": text}],
)
except InstructorRetryException as e:
print(f"Failed after retries: {e}")
except Exception as e:
print(f"Error: {e}")
Conclusion
Instructor is an incredibly useful library for anyone working with LLMs who needs structured output. By leveraging Pydantic, Instructor provides type safety, automatic validation, and retry logic that makes integrating LLMs into production applications far more reliable.
Key features we covered in this tutorial:
- Basic usage with OpenAI and other providers
- Nested Pydantic models for complex data
- Custom validation with field and model validators
- Automatic retry logic when validation fails
- Streaming with Partial objects
- Practical examples for data extraction and classification
Start with simple use cases like data extraction or classification, then gradually increase complexity as your project requires. Instructor will become an invaluable tool in your AI engineering toolkit.