Introduction to Instructor: Structured Extraction from LLMs
Large Language Models (LLMs) are incredibly powerful at understanding and generating text, but when building applications, developers rarely need raw text. They need structured data—like JSON objects—that can be easily integrated into databases, APIs, and application logic. Traditionally, getting an LLM to output reliable JSON involved complex prompt engineering, fragile parsing, and manual error handling.
Instructor is a Python library that solves this problem by bridging the gap between LLMs and structured data. It leverages Pydantic to define the exact shape of the data you want, and it patches your LLM client to ensure the model returns data that strictly adheres to that schema.
Why Instructor Matters
Using Instructor provides several critical benefits for developers:
- Type Safety: You get fully typed Python objects back from the LLM, complete with IDE autocompletion and static analysis support.
- Reduced Boilerplate: No more writing custom JSON parsers or regex to clean up LLM outputs.
- Automatic Retries: If the LLM fails to provide valid data (e.g., missing a required field or returning a string instead of an integer), Instructor automatically feeds the validation error back to the LLM and asks it to correct itself.
- Model Agnostic: Instructor supports multiple providers, including OpenAI, Anthropic, Cohere, and open-source models via Ollama or LiteLLM.
Getting Started with Instructor
To begin using Instructor, you need to install the library along with your preferred LLM provider's SDK and Pydantic. In this tutorial, we will use OpenAI as the primary example.
pip install instructor openai pydantic
Defining Data Models with Pydantic
The core of Instructor is the Pydantic model. You define a class that inherits from BaseModel and declare the fields you want the LLM to extract. You can also use Pydantic's Field to provide descriptions, which act as additional instructions for the LLM.
from pydantic import BaseModel, Field
from typing import List
class UserDetail(BaseModel):
"""Extracted user information."""
name: str = Field(..., description="The full name of the user")
age: int = Field(..., description="The age of the user in years")
interests: List[str] = Field(default_factory=list, description="A list of the user's hobbies or interests")
Your First Structured Extraction
To use the model, you first patch the OpenAI client using instructor.from_openai(). Then, you pass your Pydantic model to the response_model parameter in the create method.
import instructor
from openai import OpenAI
from pydantic import BaseModel, Field
# 1. Define the model
class UserDetail(BaseModel):
name: str = Field(..., description="The full name of the user")
age: int = Field(..., description="The age of the user in years")
# 2. Patch the client
client = instructor.from_openai(OpenAI())
# 3. Extract the data
text_block = "John Doe is a 34-year-old software engineer who loves hiking."
user = client.chat.completions.create(
model="gpt-4o-mini",
response_model=UserDetail,
messages=[
{"role": "user", "content": text_block}
]
)
print(type(user)) # <class '__main__.UserDetail'>
print(user.name) # John Doe
print(user.age) # 34
Notice that we did not have to ask the LLM to output JSON, nor did we have to parse a JSON string. Instructor handles all of this under the hood and returns a fully instantiated UserDetail object.
Advanced Techniques
Handling Lists and Nested Objects
Real-world data is rarely flat. Instructor handles complex, nested structures effortlessly. You can define models that contain other models, or models that return lists of objects.
from pydantic import BaseModel, Field
from typing import List
class Address(BaseModel):
street: str
city: str
zip_code: str
class Company(BaseModel):
name: str
industry: str
headquarters: Address
employees: int
# Extracting a list of companies from a text
class CompanyList(BaseModel):
companies: List[Company]
# Usage:
# client.chat.completions.create(
# model="gpt-4o-mini",
# response_model=CompanyList,
# messages=[{"role": "user", "content": "Extract companies from this text..."}]
# )
Validation and Retries
One of Instructor's most powerful features is its retry mechanism. If the LLM returns data that fails Pydantic validation, Instructor intercepts the error, appends the validation error message to the conversation, and prompts the LLM to try again.
You can control this behavior using the max_retries parameter. You can also add custom validation logic using Pydantic's field_validator or model_validator.
from pydantic import BaseModel, field_validator
class StrictUser(BaseModel):
name: str
age: int
@field_validator("age")
def check_age(cls, v):
if v < 0 or v > 120:
raise ValueError("Age must be between 0 and 120")
return v
# If the LLM returns age=-5, Instructor will tell the LLM:
# "Validation failed: Age must be between 0 and 120. Please fix the output."
# and retry up to max_retries times.
user = client.chat.completions.create(
model="gpt-4o-mini",
response_model=StrictUser,
max_retries=3,
messages=[{"role": "user", "content": "Extract: Jane is -5 years old."}]
)
Best Practices for Production
To get the most out of Instructor in a production environment, consider the following best practices:
- Write clear descriptions: The
descriptionparameter inFieldis your primary way of guiding the LLM. Be explicit about what you want. If a field should only contain specific values, mention them in the description. - Keep models simple: Avoid creating massive, monolithic Pydantic models with dozens of fields. Break them down into smaller, nested objects. LLMs perform better when extracting smaller chunks of structured data.
- Use Enums for constrained choices: If a field can only be one of a few values (e.g., "positive", "negative", "neutral"), use Python's
Enum. Instructor will pass the allowed values to the LLM, drastically reducing hallucinations. - Set sensible retry limits: While
max_retries=3is a good default, be mindful of token costs. If a model consistently fails validation, it might be a sign that your prompt or schema is too complex. - Leverage partial responses: For long extractions, consider using
create_partialto stream the object as it is being built. This improves perceived latency in user-facing applications.
Conclusion
Instructor fundamentally changes how developers interact with Large Language Models. By treating LLMs as data extraction engines that return typed Python objects rather than unpredictable text generators, it brings reliability and developer experience to AI applications. By combining the robust validation of Pydantic with intelligent retry mechanisms, Instructor allows you to build production-ready pipelines that you can actually trust. Whether you are building a simple data scraper or a complex autonomous agent, Instructor is an essential tool for bridging the gap between unstructured language and structured code.