Install any skill in seconds. Free to start, no credit card required.
Get Started Free →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
.claude/skills/openlair-instructor/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-06 | ✗→✓ | ▲ Improved | 296% | 0% |
| case-16 | ✗→✓ | ▲ Improved | 404% | 0% |
| case-23 | ✓→✓ | = Same ✓ | 1122% | 0% |
| case-01 | ✓→✓ | = Same ✓ | 400% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 164% | 0% |
Use Instructor when you need to:
GitHub Stars: 15,000+ | Battle-tested: 100,000+ developers
bash# Base installation pip install instructor # With specific providers pip install "instructor[anthropic]" # Anthropic Claude pip install "instructor[openai]" # OpenAI pip install "instructor[all]" # All providers
pythonimport instructor from pydantic import BaseModel from anthropic import Anthropic # Define output structure class User(BaseModel): name: str age: int email: str # Create instructor client client = instructor.from_anthropic(Anthropic()) # Extract structured data user = 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) # 30 print(user.email) # "john@example.com"
pythonfrom openai import OpenAI client = 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"}] )
Response models define the structure and validation rules for LLM outputs.
pythonfrom pydantic import BaseModel, Field class 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:
pythonclass Address(BaseModel): street: str city: str country: str class Person(BaseModel): name: str age: int address: Address # Nested model person = 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"
pythonfrom typing import Optional class Product(BaseModel): name: str price: float discount: Optional[float] = None # Optional description: str = Field(default="No description") # Default value # LLM doesn't need to provide discount or description
pythonfrom enum import Enum class Sentiment(str, Enum): POSITIVE = "positive" NEGATIVE = "negative" NEUTRAL = "neutral" class Review(BaseModel): text: str sentiment: Sentiment # Only these 3 values allowed review = 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
Pydantic validates LLM outputs automatically. If validation fails, Instructor retries.
pythonfrom pydantic import Field, EmailStr, HttpUrl class Contact(BaseModel): name: str = Field(min_length=2, max_length=100) age: int = Field(ge=0, le=120) # 0 <= age <= 120 email: EmailStr # Validates email format website: HttpUrl # Validates URL format # If LLM provides invalid data, Instructor retries automatically
pythonfrom pydantic import field_validator class Event(BaseModel): name: str date: str attendees: int @field_validator('date') def validate_date(cls, v): """Ensure date is in YYYY-MM-DD format.""" import re if 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
pythonfrom pydantic import model_validator class DateRange(BaseModel): start_date: str end_date: str @model_validator(mode='after') def check_dates(self): """Ensure end_date is after start_date.""" from datetime import datetime start = 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
Instructor retries automatically when validation fails, providing error feedback to the LLM.
python# Retries up to 3 times if validation fails user = 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:
Stream partial results for real-time processing.
pythonfrom instructor import Partial class Story(BaseModel): title: str content: str tags: list[str] # Stream partial updates as LLM generates for 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
pythonclass Task(BaseModel): title: str priority: str # Stream list items as they're generated tasks = 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
pythonimport instructor from anthropic import Anthropic client = instructor.from_anthropic( Anthropic(api_key="your-api-key") ) # Use with Claude models response = client.messages.create( model="claude-sonnet-4-5-20250929", max_tokens=1024, messages=[...], response_model=YourModel )
pythonfrom openai import OpenAI client = instructor.from_openai( OpenAI(api_key="your-api-key") ) response = client.chat.completions.create( model="gpt-4o-mini", response_model=YourModel, messages=[...] )
pythonfrom openai import OpenAI # Point to local Ollama server client = 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=[...] )
pythonclass CompanyInfo(BaseModel): name: str founded_year: int industry: str employees: int headquarters: str text = """ Tesla, Inc. was founded in 2003. It operates in the automotive and energy industry with approximately 140,000 employees. The company is headquartered in 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 )
pythonclass Category(str, Enum): TECHNOLOGY = "technology" FINANCE = "finance" HEALTHCARE = "healthcare" EDUCATION = "education" OTHER = "other" class ArticleClassification(BaseModel): category: Category confidence: 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 )
pythonclass Person(BaseModel): name: str role: str class Organization(BaseModel): name: str industry: str class 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}")
pythonclass SentimentAnalysis(BaseModel): overall_sentiment: Sentiment positive_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 )
pythondef 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]
pythonfrom typing import Union class TextContent(BaseModel): type: str = "text" content: str class ImageContent(BaseModel): type: str = "image" url: HttpUrl caption: str class Post(BaseModel): title: str content: Union[TextContent, ImageContent] # Either type # LLM chooses appropriate type based on content
pythonfrom pydantic import create_model # Create model at runtime DynamicUser = 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 )
python# For providers without native structured outputs client = 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)
python# Single-use client with 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
pythonfrom pydantic import ValidationError try: 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 gracefully except Exception as e: print(f"API error: {e}")
pythonclass 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 messages json_schema_extra = { "examples": [ { "name": "John Doe", "age": 30, "email": "john@example.com" } ] }
python# ❌ Bad: Vague class Product(BaseModel): name: str price: float # ✅ Good: Descriptive class Product(BaseModel): name: str = Field(description="Product name from the text") price: float = Field(description="Price in USD, without currency symbol")
python# ✅ Good: Constrain values class 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")
pythonmessages = [{ "role": "user", "content": """Extract person info from: "John, 30, engineer" Example format: { "name": "John Doe", "age": 30, "occupation": "engineer" }""" }]
python# ✅ Good: Enum ensures valid values class Status(str, Enum): PENDING = "pending" APPROVED = "approved" REJECTED = "rejected" class Application(BaseModel): status: Status # LLM must choose from enum
pythonclass PartialData(BaseModel): required_field: str optional_field: Optional[str] = None default_field: str = "default_value" # LLM only needs to provide required_field
| 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:
When to choose alternatives:
references/validation.md - Advanced validation patternsreferences/providers.md - Provider-specific configurationreferences/examples.md - Real-world use cases| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-23 | pass→pass | 2,215 | 2,604 | +18% | 1 | 1 | 0% | 435 | 5,314 | +1122% | 0 | 0 | — |
case-01 | pass→pass | 5,587 | 3,818 | -32% | 1 | 1 | 0% | 1,149 | 5,750 | +400% | 0 | 0 | — |
case-02 | pass→pass | 11,484 | 4,625 | -60% | 1 | 1 | 0% | 2,234 | 5,900 | +164% | 0 | 0 | — |
case-03 | pass→pass | 5,900 | 2,181 | -63% | 1 | 1 | 0% | 1,119 | 5,198 | +365% | 0 | 0 | — |
case-04 | pass→pass | 5,932 | 2,375 | -60% | 1 | 1 | 0% | 1,128 | 5,296 | +370% | 0 | 0 | — |
case-05 | pass→pass | 9,339 | 2,678 | -71% | 1 | 1 | 0% | 1,984 | 5,384 | +171% | 0 | 0 | — |
case-06 | fail→pass | 7,877 | 4,816 | -39% | 1 | 1 | 0% | 1,440 | 5,706 | +296% | 0 | 0 | — |
case-07 | pass→pass | 7,153 | 4,490 | -37% | 1 | 1 | 0% | 1,402 | 5,732 | +309% | 0 | 0 | — |
case-08 | pass→pass | 5,950 | 1,993 | -67% | 1 | 1 | 0% | 1,092 | 5,191 | +375% | 0 | 0 | — |
case-09 | pass→pass | 2,833 | 2,476 | -13% | 1 | 1 | 0% | 579 | 5,303 | +816% | 0 | 0 | — |
case-10 | pass→pass | 3,507 | 3,149 | -10% | 1 | 1 | 0% | 684 | 5,446 | +696% | 0 | 0 | — |
case-11 | pass→pass | 9,247 | 6,990 | -24% | 1 | 1 | 0% | 1,827 | 6,264 | +243% | 0 | 0 | — |
case-12 | pass→pass | 12,000 | 4,697 | -61% | 1 | 1 | 0% | 2,255 | 5,862 | +160% | 0 | 0 | — |
case-13 | pass→pass | 8,158 | 5,699 | -30% | 1 | 1 | 0% | 1,640 | 5,953 | +263% | 0 | 0 | — |
case-14 | pass→pass | 6,912 | 5,765 | -17% | 1 | 1 | 0% | 1,375 | 5,976 | +335% | 0 | 0 | — |
case-15 | pass→pass | 4,089 | 3,972 | -3% | 1 | 1 | 0% | 754 | 5,594 | +642% | 0 | 0 | — |
case-16 | fail→pass | 6,898 | 3,600 | -48% | 1 | 1 | 0% | 1,095 | 5,520 | +404% | 0 | 0 | — |
case-17 | pass→pass | 8,232 | 5,150 | -37% | 1 | 1 | 0% | 1,667 | 5,791 | +247% | 0 | 0 | — |
case-18 | pass→pass | 4,828 | 3,995 | -17% | 1 | 1 | 0% | 982 | 5,550 | +465% | 0 | 0 | — |
case-19 | pass→pass | 3,147 | 3,431 | +9% | 1 | 1 | 0% | 578 | 5,506 | +853% | 0 | 0 | — |
case-20 | pass→pass | 7,980 | 4,809 | -40% | 1 | 1 | 0% | 1,524 | 5,691 | +273% | 0 | 0 | — |
case-21 | pass→pass | 8,282 | 8,032 | -3% | 1 | 1 | 0% | 1,585 | 6,375 | +302% | 0 | 0 | — |
case-22 | pass→pass | 11,253 | 7,946 | -29% | 1 | 1 | 0% | 1,962 | 6,200 | +216% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of +9 percentage points is the difference between those two pass rates over the 23 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.