title: Instructor Philosophy - Design Principles and Approach description: Understand the core philosophy behind Instructor's design. Learn about type safety, validation, and the principles that guide structured LLM output development.
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.
import instructor
from pydantic import BaseModel
# What you already know (Pydantic)
class User(BaseModel):
name: str
age: int
# What Instructor adds
client = instructor.from_provider("openai/gpt-4.1-mini")
_user = client.create(
response_model=User,
messages=[{"role": "user", "content": "Jane is 33"}],
) # That's it
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:
import instructor
from pydantic import BaseModel
class User(BaseModel):
name: str
age: int
# With Instructor
client = instructor.from_provider("openai/gpt-4.1-mini")
_result = client.create(
response_model=User,
messages=[{"role": "user", "content": "Jane is 33"}],
)
# Want to go back to raw API? Just remove response_model:
client = instructor.from_provider("openai/gpt-4.1-mini")
_result = client.create(messages=[{"role": "user", "content": "Say hello"}])
# Or use the provider directly:
from openai import OpenAI
_raw_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.
import instructor
from pydantic import BaseModel
class User(BaseModel):
name: str
age: int
# See exactly what Instructor sends
instructor.logfire.configure() # Full observability
client = instructor.from_provider("openai/gpt-4.1-mini")
result = client.create(
response_model=User,
messages=[{"role": "user", "content": "Jane is 33"}],
)
# Access raw responses
_raw_response = 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.
import instructor
from pydantic import BaseModel
client = instructor.from_provider("openai/gpt-4.1-mini")
class User(BaseModel):
name: str
age: int
class Company(BaseModel):
name: str
industry: str
class Analysis(BaseModel):
user: User
company: Company
# Build complex systems with simple functions
def extract_user(text: str) -> User:
return client.create(
response_model=User, messages=[{"role": "user", "content": text}]
)
def extract_company(text: str) -> Company:
return client.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
_analysis = analyze_email("Please introduce Jane from Acme.")
Start simple, grow naturally¶
The best code is code that grows with your needs:
import instructor
from instructor import Partial
from pydantic import BaseModel, field_validator
client = instructor.from_provider("openai/gpt-4.1-mini")
class User(BaseModel):
name: str
age: int
# Day 1: Just get it working
_user = client.create(
response_model=User,
messages=[{"role": "user", "content": "Jane is 33"}],
)
# Day 7: Add validation
class User(BaseModel):
name: str
age: int
@field_validator("age")
def check_age(cls, value: int) -> int:
if value < 0 or value > 150:
raise ValueError("Invalid age")
return value
# Day 14: Add retries for production
_user = client.create(
response_model=User,
messages=[{"role": "user", "content": "Jane is 33"}],
max_retries=3,
)
# Day 30: Add streaming for better UX
def update_ui(_partial: Partial[User]) -> None:
pass
for partial in client.create(
response_model=Partial[User],
messages=[{"role": "user", "content": "Jane is 33"}],
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: str):
# ...
# You write your own prompts:
text = "Jane is 33"
_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.
import instructor
from pydantic import BaseModel
# We DON'T do this:
# class UserExtractionAgent(instructor.Agent):
# tools = [instructor.WebSearch(), instructor.Calculator()]
class User(BaseModel):
name: str
age: int
def search_web(query: str) -> str:
return f"Results for {query}"
client = instructor.from_provider("openai/gpt-4.1-mini")
# You build what makes sense:
def extract_user_with_search(query: str) -> User:
# Your logic, your way
search_results = search_web(query)
return client.create(
response_model=User, messages=[{"role": "user", "content": search_results}]
)
_user = extract_user_with_search("Find Jane")
No framework lock-in¶
Your code should work with or without us:
import instructor
from pydantic import BaseModel
# 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}
client = instructor.from_provider("openai/gpt-4.1-mini")
# Instructor just connects them to LLMs
user = client.create(
response_model=User,
messages=[{"role": "user", "content": "Jane is 33"}],
)
_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:
from enum import Enum
from typing import List
import instructor
from pydantic import BaseModel
# 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.1-mini")
# Extract structured data (simple function call)
tickets = client.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.