Files
567-labs--instructor/docs/concepts/philosophy.md
T
wehub-resource-sync 97e91a83f3
Ruff / Ruff (push) Has been cancelled
Test / Core Tests (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.10) (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.11) (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.12) (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.13) (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.9) (push) Has been cancelled
Test / Full Coverage (Python 3.11) (push) Has been cancelled
Test / Core Provider Tests (OpenAI) (push) Has been cancelled
Test / Core Provider Tests (Anthropic) (push) Has been cancelled
Test / Core Provider Tests (Google) (push) Has been cancelled
Test / Core Provider Tests (Other) (push) Has been cancelled
Test / Anthropic Tests (push) Has been cancelled
Test / Gemini Tests (push) Has been cancelled
Test / Google GenAI Tests (push) Has been cancelled
Test / Vertex AI Tests (push) Has been cancelled
Test / OpenAI Tests (push) Has been cancelled
Test / Writer Tests (push) Has been cancelled
Test / Auto Client Tests (push) Has been cancelled
ty / type-check (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:36:38 +08:00

355 lines
7.6 KiB
Markdown

---
title: Philosophy
description: The principles behind Instructor - why simple beats complex every time.
---
# 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.
```python
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:
```python
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.
```python
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.
```python
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:
```python
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.
```python
# 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.
```python
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:
```python
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:
```python
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.