# 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.
