Skill v1.0.0
currentAutomated scan100/100name: instructor description: Extract structured data from LLM responses with Pydantic validation, retry failed extractions automatically, parse complex JSON with type safety, and stream partial results with Instructor - battle-tested structured output library version: 1.0.0 author: Orchestra Research license: MIT dependencies: [instructor, pydantic, openai, anthropic] platforms: [linux, macos, windows] metadata: hermes: tags: [Prompt Engineering, Instructor, Structured Output, Pydantic, Data Extraction, JSON Parsing, Type Safety, Validation, Streaming, OpenAI, Anthropic]
Instructor: Structured LLM Outputs
When to Use This Skill
Use Instructor when you need to:
- Extract structured data from LLM responses reliably
- Validate outputs against Pydantic schemas automatically
- Retry failed extractions with automatic error handling
- Parse complex JSON with type safety and validation
- Stream partial results for real-time processing
- Support multiple LLM providers with consistent API
GitHub Stars: 15,000+ | Battle-tested: 100,000+ developers
Installation
# Base installationpip install instructor# With specific providerspip install "instructor[anthropic]" # Anthropic Claudepip install "instructor[openai]" # OpenAIpip install "instructor[all]" # All providers
Quick Start
Basic Example: Extract User Data
import instructorfrom pydantic import BaseModelfrom anthropic import Anthropic# Define output structureclass User(BaseModel):name: strage: intemail: str# Create instructor clientclient = instructor.from_anthropic(Anthropic())# Extract structured datauser = client.messages.create(model="claude-sonnet-4-5-20250929",max_tokens=1024,messages=[{"role": "user","content": "John Doe is 30 years old. His email is john@example.com"}],response_model=User)print(user.name) # "John Doe"print(user.age) # 30print(user.email) # "john@example.com"
With OpenAI
from openai import OpenAIclient = instructor.from_openai(OpenAI())user = client.chat.completions.create(model="gpt-4o-mini",response_model=User,messages=[{"role": "user", "content": "Extract: Alice, 25, alice@email.com"}])
Core Concepts
1. Response Models (Pydantic)
Response models define the structure and validation rules for LLM outputs.
Basic Model
from pydantic import BaseModel, Fieldclass Article(BaseModel):title: str = Field(description="Article title")author: str = Field(description="Author name")word_count: int = Field(description="Number of words", gt=0)tags: list[str] = Field(description="List of relevant tags")article = client.messages.create(model="claude-sonnet-4-5-20250929",max_tokens=1024,messages=[{"role": "user","content": "Analyze this article: [article text]"}],response_model=Article)
Benefits:
- Type safety with Python type hints
- Automatic validation (word_count > 0)
- Self-documenting with Field descriptions
- IDE autocomplete support
Nested Models
class Address(BaseModel):street: strcity: strcountry: strclass Person(BaseModel):name: strage: intaddress: Address # Nested modelperson = client.messages.create(model="claude-sonnet-4-5-20250929",max_tokens=1024,messages=[{"role": "user","content": "John lives at 123 Main St, Boston, USA"}],response_model=Person)print(person.address.city) # "Boston"
Optional Fields
from typing import Optionalclass Product(BaseModel):name: strprice: floatdiscount: Optional[float] = None # Optionaldescription: str = Field(default="No description") # Default value# LLM doesn't need to provide discount or description
Enums for Constraints
from enum import Enumclass Sentiment(str, Enum):POSITIVE = "positive"NEGATIVE = "negative"NEUTRAL = "neutral"class Review(BaseModel):text: strsentiment: Sentiment # Only these 3 values allowedreview = client.messages.create(model="claude-sonnet-4-5-20250929",max_tokens=1024,messages=[{"role": "user","content": "This product is amazing!"}],response_model=Review)print(review.sentiment) # Sentiment.POSITIVE
2. Validation
Pydantic validates LLM outputs automatically. If validation fails, Instructor retries.
Built-in Validators
from pydantic import Field, EmailStr, HttpUrlclass Contact(BaseModel):name: str = Field(min_length=2, max_length=100)age: int = Field(ge=0, le=120) # 0 <= age <= 120email: EmailStr # Validates email formatwebsite: HttpUrl # Validates URL format# If LLM provides invalid data, Instructor retries automatically
Custom Validators
from pydantic import field_validatorclass Event(BaseModel):name: strdate: strattendees: int@field_validator('date')def validate_date(cls, v):"""Ensure date is in YYYY-MM-DD format."""import reif not re.match(r'\d{4}-\d{2}-\d{2}', v):raise ValueError('Date must be YYYY-MM-DD format')return v@field_validator('attendees')def validate_attendees(cls, v):"""Ensure positive attendees."""if v < 1:raise ValueError('Must have at least 1 attendee')return v
Model-Level Validation
from pydantic import model_validatorclass DateRange(BaseModel):start_date: strend_date: str@model_validator(mode='after')def check_dates(self):"""Ensure end_date is after start_date."""from datetime import datetimestart = datetime.strptime(self.start_date, '%Y-%m-%d')end = datetime.strptime(self.end_date, '%Y-%m-%d')if end < start:raise ValueError('end_date must be after start_date')return self
3. Automatic Retrying
Instructor retries automatically when validation fails, providing error feedback to the LLM.
# Retries up to 3 times if validation failsuser = client.messages.create(model="claude-sonnet-4-5-20250929",max_tokens=1024,messages=[{"role": "user","content": "Extract user from: John, age unknown"}],response_model=User,max_retries=3 # Default is 3)# If age can't be extracted, Instructor tells the LLM:# "Validation error: age - field required"# LLM tries again with better extraction
How it works:
- LLM generates output
- Pydantic validates
- If invalid: Error message sent back to LLM
- LLM tries again with error feedback
- Repeats up to max_retries
4. Streaming
Stream partial results for real-time processing.
Streaming Partial Objects
from instructor import Partialclass Story(BaseModel):title: strcontent: strtags: list[str]# Stream partial updates as LLM generatesfor partial_story in client.messages.create_partial(model="claude-sonnet-4-5-20250929",max_tokens=1024,messages=[{"role": "user","content": "Write a short sci-fi story"}],response_model=Story):print(f"Title: {partial_story.title}")print(f"Content so far: {partial_story.content[:100]}...")# Update UI in real-time
Streaming Iterables
class Task(BaseModel):title: strpriority: str# Stream list items as they're generatedtasks = client.messages.create_iterable(model="claude-sonnet-4-5-20250929",max_tokens=1024,messages=[{"role": "user","content": "Generate 10 project tasks"}],response_model=Task)for task in tasks:print(f"- {task.title} ({task.priority})")# Process each task as it arrives
Provider Configuration
Anthropic Claude
import instructorfrom anthropic import Anthropicclient = instructor.from_anthropic(Anthropic(api_key="your-api-key"))# Use with Claude modelsresponse = client.messages.create(model="claude-sonnet-4-5-20250929",max_tokens=1024,messages=[...],response_model=YourModel)
OpenAI
from openai import OpenAIclient = instructor.from_openai(OpenAI(api_key="your-api-key"))response = client.chat.completions.create(model="gpt-4o-mini",response_model=YourModel,messages=[...])
Local Models (Ollama)
from openai import OpenAI# Point to local Ollama serverclient = instructor.from_openai(OpenAI(base_url="http://localhost:11434/v1",api_key="ollama" # Required but ignored),mode=instructor.Mode.JSON)response = client.chat.completions.create(model="llama3.1",response_model=YourModel,messages=[...])
Common Patterns
Pattern 1: Data Extraction from Text
class CompanyInfo(BaseModel):name: strfounded_year: intindustry: stremployees: intheadquarters: strtext = """Tesla, Inc. was founded in 2003. It operates in the automotive and energyindustry with approximately 140,000 employees. The company is headquarteredin Austin, Texas."""company = client.messages.create(model="claude-sonnet-4-5-20250929",max_tokens=1024,messages=[{"role": "user","content": f"Extract company information from: {text}"}],response_model=CompanyInfo)
Pattern 2: Classification
class Category(str, Enum):TECHNOLOGY = "technology"FINANCE = "finance"HEALTHCARE = "healthcare"EDUCATION = "education"OTHER = "other"class ArticleClassification(BaseModel):category: Categoryconfidence: float = Field(ge=0.0, le=1.0)keywords: list[str]classification = client.messages.create(model="claude-sonnet-4-5-20250929",max_tokens=1024,messages=[{"role": "user","content": "Classify this article: [article text]"}],response_model=ArticleClassification)
Pattern 3: Multi-Entity Extraction
class Person(BaseModel):name: strrole: strclass Organization(BaseModel):name: strindustry: strclass Entities(BaseModel):people: list[Person]organizations: list[Organization]locations: list[str]text = "Tim Cook, CEO of Apple, announced at the event in Cupertino..."entities = client.messages.create(model="claude-sonnet-4-5-20250929",max_tokens=1024,messages=[{"role": "user","content": f"Extract all entities from: {text}"}],response_model=Entities)for person in entities.people:print(f"{person.name} - {person.role}")
Pattern 4: Structured Analysis
class SentimentAnalysis(BaseModel):overall_sentiment: Sentimentpositive_aspects: list[str]negative_aspects: list[str]suggestions: list[str]score: float = Field(ge=-1.0, le=1.0)review = "The product works well but setup was confusing..."analysis = client.messages.create(model="claude-sonnet-4-5-20250929",max_tokens=1024,messages=[{"role": "user","content": f"Analyze this review: {review}"}],response_model=SentimentAnalysis)
Pattern 5: Batch Processing
def extract_person(text: str) -> Person:return client.messages.create(model="claude-sonnet-4-5-20250929",max_tokens=1024,messages=[{"role": "user","content": f"Extract person from: {text}"}],response_model=Person)texts = ["John Doe is a 30-year-old engineer","Jane Smith, 25, works in marketing","Bob Johnson, age 40, software developer"]people = [extract_person(text) for text in texts]
Advanced Features
Union Types
from typing import Unionclass TextContent(BaseModel):type: str = "text"content: strclass ImageContent(BaseModel):type: str = "image"url: HttpUrlcaption: strclass Post(BaseModel):title: strcontent: Union[TextContent, ImageContent] # Either type# LLM chooses appropriate type based on content
Dynamic Models
from pydantic import create_model# Create model at runtimeDynamicUser = create_model('User',name=(str, ...),age=(int, Field(ge=0)),email=(EmailStr, ...))user = client.messages.create(model="claude-sonnet-4-5-20250929",max_tokens=1024,messages=[...],response_model=DynamicUser)
Custom Modes
# For providers without native structured outputsclient = instructor.from_anthropic(Anthropic(),mode=instructor.Mode.JSON # JSON mode)# Available modes:# - Mode.ANTHROPIC_TOOLS (recommended for Claude)# - Mode.JSON (fallback)# - Mode.TOOLS (OpenAI tools)
Context Management
# Single-use clientwith instructor.from_anthropic(Anthropic()) as client:result = client.messages.create(model="claude-sonnet-4-5-20250929",max_tokens=1024,messages=[...],response_model=YourModel)# Client closed automatically
Error Handling
Handling Validation Errors
from pydantic import ValidationErrortry:user = client.messages.create(model="claude-sonnet-4-5-20250929",max_tokens=1024,messages=[...],response_model=User,max_retries=3)except ValidationError as e:print(f"Failed after retries: {e}")# Handle gracefullyexcept Exception as e:print(f"API error: {e}")
Custom Error Messages
class ValidatedUser(BaseModel):name: str = Field(description="Full name, 2-100 characters")age: int = Field(description="Age between 0 and 120", ge=0, le=120)email: EmailStr = Field(description="Valid email address")class Config:# Custom error messagesjson_schema_extra = {"examples": [{"name": "John Doe","age": 30,"email": "john@example.com"}]}
Best Practices
1. Clear Field Descriptions
# ❌ Bad: Vagueclass Product(BaseModel):name: strprice: float# ✅ Good: Descriptiveclass Product(BaseModel):name: str = Field(description="Product name from the text")price: float = Field(description="Price in USD, without currency symbol")
2. Use Appropriate Validation
# ✅ Good: Constrain valuesclass Rating(BaseModel):score: int = Field(ge=1, le=5, description="Rating from 1 to 5 stars")review: str = Field(min_length=10, description="Review text, at least 10 chars")
3. Provide Examples in Prompts
messages = [{"role": "user","content": """Extract person info from: "John, 30, engineer"Example format:{"name": "John Doe","age": 30,"occupation": "engineer"}"""}]
4. Use Enums for Fixed Categories
# ✅ Good: Enum ensures valid valuesclass Status(str, Enum):PENDING = "pending"APPROVED = "approved"REJECTED = "rejected"class Application(BaseModel):status: Status # LLM must choose from enum
5. Handle Missing Data Gracefully
class PartialData(BaseModel):required_field: stroptional_field: Optional[str] = Nonedefault_field: str = "default_value"# LLM only needs to provide required_field
Comparison to Alternatives
| Feature | Instructor | Manual JSON | LangChain | DSPy | |
|---|---|---|---|---|---|
| Type Safety | ✅ Yes | ❌ No | ⚠️ Partial | ✅ Yes | |
| Auto Validation | ✅ Yes | ❌ No | ❌ No | ⚠️ Limited | |
| Auto Retry | ✅ Yes | ❌ No | ❌ No | ✅ Yes | |
| Streaming | ✅ Yes | ❌ No | ✅ Yes | ❌ No | |
| Multi-Provider | ✅ Yes | ⚠️ Manual | ✅ Yes | ✅ Yes | |
| Learning Curve | Low | Low | Medium | High |
When to choose Instructor:
- Need structured, validated outputs
- Want type safety and IDE support
- Require automatic retries
- Building data extraction systems
When to choose alternatives:
- DSPy: Need prompt optimization
- LangChain: Building complex chains
- Manual: Simple, one-off extractions
Resources
- Documentation: https://python.useinstructor.com
- GitHub: https://github.com/jxnl/instructor (15k+ stars)
- Cookbook: https://python.useinstructor.com/examples
- Discord: Community support available
See Also
references/validation.md- Advanced validation patternsreferences/providers.md- Provider-specific configurationreferences/examples.md- Real-world use cases