Philosophy¶
Great tools make hard things easy without making easy things hard. That's Instructor.
Start with what developers know¶
Most AI frameworks invent their own abstractions. We don't.
# What you already know (Pydantic)
class User(BaseModel):
name: str
age: int
# What Instructor adds
client = instructor.from_provider("openai/gpt-4")
user = client.chat.completions.create(
response_model=User, # That's it
messages=[...]
)
If you know Pydantic, you know Instructor. No new concepts, no new syntax, no 200-page manual.
Your escape hatch is always there¶
The worst frameworks are roach motels - easy to get in, impossible to get out. Instructor is different:
# With Instructor
client = instructor.from_provider("openai/gpt-4")
result = client.chat.completions.create(
response_model=User,
messages=[...]
)
# Want to go back to raw API? Just remove response_model:
client = instructor.from_provider("openai/gpt-4")
result = client.chat.completions.create(
messages=[...] # Now you get the raw response
)
# Or use the provider directly:
from openai import OpenAI
client = OpenAI() # Back to vanilla
We patch, we don't wrap. Your code, your control.
Show, don't hide¶
Bad frameworks hide complexity. Good tools help you understand it.
# See exactly what Instructor sends
import instructor
instructor.logfire.configure() # Full observability
# Access raw responses
result = client.chat.completions.create(
response_model=User,
messages=[...],
)
print(result._raw_response) # See what the LLM actually returned
When something goes wrong (and it will), you can see exactly what happened.
Composition beats configuration¶
No YAML files. No decorators. No magic. Just functions.
# Build complex systems with simple functions
def extract_user(text: str) -> User:
return client.chat.completions.create(
response_model=User,
messages=[{"role": "user", "content": text}]
)
def extract_company(text: str) -> Company:
return client.chat.completions.create(
response_model=Company,
messages=[{"role": "user", "content": text}]
)
def analyze_email(email: str) -> Analysis:
user = extract_user(email)
company = extract_company(email)
return Analysis(user=user, company=company)
# Compose however makes sense for YOUR application
Start simple, grow naturally¶
The best code is code that grows with your needs:
# Day 1: Just get it working
user = client.chat.completions.create(
response_model=User,
messages=[{"role": "user", "content": "..."}]
)
# Day 7: Add validation
class User(BaseModel):
name: str
age: int
@field_validator('age')
def check_age(cls, v):
if v < 0 or v > 150:
raise ValueError('Invalid age')
return v
# Day 14: Add retries for production
user = client.chat.completions.create(
response_model=User,
messages=[{"role": "user", "content": "..."}],
max_retries=3
)
# Day 30: Add streaming for better UX
for partial in client.chat.completions.create(
response_model=Partial[User],
messages=[{"role": "user", "content": "..."}],
stream=True
):
update_ui(partial)
Each addition is one line. No refactoring. No migration guide.
What we intentionally DON'T do¶
No prompt engineering¶
We don't write prompts for you. You know your domain better than we do.
# We DON'T do this:
@instructor.prompt("Extract the user information carefully")
def extract_user(text):
...
# You write your own prompts:
messages = [{
"role": "system",
"content": "You are a precise data extractor"
}, {
"role": "user",
"content": f"Extract user from: {text}"
}]
No new abstractions¶
We don't invent concepts like "Agents", "Chains", or "Tools". Those are your domain concepts.
# We DON'T do this:
class UserExtractionAgent(instructor.Agent):
tools = [instructor.WebSearch(), instructor.Calculator()]
# You build what makes sense:
def extract_user_with_search(query: str) -> User:
# Your logic, your way
search_results = search_web(query)
return client.chat.completions.create(
response_model=User,
messages=[{"role": "user", "content": search_results}]
)
No framework lock-in¶
Your code should work with or without us:
# This is just a Pydantic model
class User(BaseModel):
name: str
age: int
# This is just a function
def process_user(user: User) -> dict:
return {"name": user.name.upper(), "adult": user.age >= 18}
# Instructor just connects them to LLMs
user = client.chat.completions.create(
response_model=User,
messages=[...]
)
result = process_user(user) # Works with or without Instructor
The result¶
By following these principles, we get:
- Tiny API surface: Learn it in minutes, not days
- Zero vendor lock-in: Switch providers or remove Instructor anytime
- Debuggable: When things break, you can see why
- Composable: Build complex systems from simple parts
- Pythonic: If it feels natural in Python, it feels natural in Instructor
In practice¶
Here's what building with Instructor actually looks like:
import instructor
from pydantic import BaseModel
from typing import List
from enum import Enum
# Your domain models (not ours)
class Priority(str, Enum):
HIGH = "high"
MEDIUM = "medium"
LOW = "low"
class Ticket(BaseModel):
title: str
description: str
priority: Priority
estimated_hours: float
# Your business logic (not ours)
def prioritize_tickets(tickets: List[Ticket]) -> List[Ticket]:
return sorted(tickets, key=lambda t: (t.priority.value, -t.estimated_hours))
# Connect to LLM (one line)
client = instructor.from_provider("openai/gpt-4")
# Extract structured data (simple function call)
tickets = client.chat.completions.create(
response_model=List[Ticket],
messages=[{
"role": "user",
"content": "Parse these support tickets: ..."
}]
)
# Use your business logic
prioritized = prioritize_tickets(tickets)
No framework. No abstractions. Just Python.
The philosophy in one sentence¶
Make structured LLM outputs as easy as defining a Pydantic model.
Everything else follows from that.