Instructor: Getting Structured Output from LLMs with Python

# Instructor: Mendapatkan Structured Output dari LLM dengan Python Salah satu tantangan terbesar saat bekerja dengan Large Language Models (LLM) adalah mendapatkan output yang terstruktur dan konsist...

By Ruby Abdullah · · tutorial
InstructorLLMPydanticStructured OutputPython

Instructor: Getting Structured Output from LLMs with Python

One of the biggest challenges when working with Large Language Models (LLMs) is getting structured and consistent output. LLMs by default produce free-form text, which is difficult to parse and integrate into applications. The Instructor library solves this problem by leveraging Pydantic for validation and structured data extraction from LLMs.

In this tutorial, we will learn how to use Instructor to get reliable JSON/Pydantic outputs from various LLM providers such as OpenAI, Anthropic, and others.

What Is Instructor?

Instructor is a Python library that patches LLM clients (like OpenAI) to return validated Pydantic objects instead of plain strings. Instructor works by leveraging function calling or JSON mode from the LLM, then validates the results using Pydantic.

Key advantages of Instructor:

  • Type-safe: Output is guaranteed to match the defined Pydantic schema
  • Automatic retry: If validation fails, Instructor automatically retries with error feedback
  • Streaming support: Supports partial streaming for complex objects
  • Multi-provider: Supports OpenAI, Anthropic, Google, Mistral, and more
  • Custom validation: You can add Pydantic validators for business logic

Installation

First, install Instructor along with the required dependencies:

pip install instructor openai pydantic

For other providers, install additional dependencies:

# For Anthropic

pip install instructor anthropic

For Google Gemini

pip install instructor google-generativeai

For Mistral

pip install instructor mistralai

Make sure you have an API key from the provider you will be using:

export OPENAIAPIKEY="sk-your-api-key-here"

Basic Usage with OpenAI

Let's start with a simple example: extracting user information from text.

import instructor

from openai import OpenAI

from pydantic import BaseModel

Patch OpenAI client with Instructor

client = instructor.fromopenai(OpenAI())

Define the output schema

class UserInfo(BaseModel):

name: str

age: int

email: str

Extract structured data from text

user = client.chat.completions.create(

model="gpt-4o-mini",

responsemodel=UserInfo,

messages=[

{

"role": "user",

"content": "My name is John Smith, I'm 28 years old. "

"My email is john.smith@email.com"

}

],

)

print(user)

UserInfo(name='John Smith', age=28, email='john.smith@email.com')

print(user.name) # John Smith

print(user.age) # 28

print(user.email) # john.smith@email.com

Notice that responsemodel=UserInfo is the key parameter that tells Instructor what schema to expect. The result is not a dictionary or string, but a validated Pydantic object.

Complex Pydantic Models

Instructor supports complex Pydantic models including nested models, optional fields, enums, and lists.

from pydantic import BaseModel, Field

from typing import Optional, List

from enum import Enum

class JobLevel(str, Enum):

JUNIOR = "junior"

MID = "mid"

SENIOR = "senior"

LEAD = "lead"

class Skill(BaseModel):

name: str = Field(description="Name of the skill or technology")

yearsexperience: int = Field(

description="Years of experience", ge=0, le=50

)

proficiency: str = Field(

description="Proficiency level: beginner, intermediate, advanced"

)

class WorkExperience(BaseModel):

company: str

role: str

durationmonths: int = Field(ge=1)

description: str

class CandidateProfile(BaseModel):

name: str

currentrole: str

level: JobLevel

totalyearsexperience: int = Field(ge=0)

skills: List[Skill]

workhistory: List[WorkExperience]

education: str

summary: str = Field(

description="Profile summary of the candidate in 2-3 sentences"

)

resumetext = """

I'm Sarah Chen, currently working as a Senior Data Engineer at Spotify

for the past 3 years. Previously I worked at Netflix as a Data Engineer

for 2 years. I have 5 years of experience in data engineering with main

expertise in Python (5 years, advanced), Apache Spark (4 years, advanced),

and SQL (5 years, advanced). I'm also familiar with Kafka (2 years,

intermediate) and Kubernetes (1 year, beginner).

BS in Computer Science from Stanford University.

"""

profile = client.chat.completions.create(

model="gpt-4o",

responsemodel=CandidateProfile,

messages=[

{

"role": "system",

"content": "Extract the candidate profile from the following resume text."

},

{"role": "user", "content": resumetext}

],

)

print(f"Name: {profile.name}")

print(f"Level: {profile.level.value}")

print(f"Skills:")

for skill in profile.skills:

print(f" - {skill.name}: {skill.yearsexperience} years "

f"({skill.proficiency})")

Validation with Pydantic Validators

One of the most powerful features of Instructor is the ability to add custom validation. If validation fails, Instructor will automatically retry and send the error message to the LLM.

from pydantic import BaseModel, Field, fieldvalidator, modelvalidator

class SQLQuery(BaseModel):

query: str = Field(description="A valid SQL query")

explanation: str = Field(description="Explanation of the query")

tablesused: List[str] = Field(description="List of tables used")

@fieldvalidator("query")

@classmethod

def validatenodelete(cls, v: str) -> str:

if "DELETE" in v.upper() or "DROP" in v.upper():

raise ValueError(

"Query must not contain DELETE or DROP for data safety"

)

return v

@fieldvalidator("query")

@classmethod

def validatehasselect(cls, v: str) -> str:

if not v.strip().upper().startswith("SELECT"):

raise ValueError("Query must start with SELECT")

return v

@modelvalidator(mode="after")

def validatetablesmatchquery(self):

for table in self.tablesused:

if table.lower() not in self.query.lower():

raise ValueError(

f"Table '{table}' not found in query"

)

return self

result = client.chat.completions.create(

model="gpt-4o-mini",

responsemodel=SQLQuery,

messages=[

{

"role": "user",

"content": "Create a query to show the top 10 customers "

"with the highest total purchases from the "

"customers and orders tables"

}

],

)

print(f"Query: {result.query}")

print(f"Explanation: {result.explanation}")

print(f"Tables: {result.tablesused}")

Retry Logic and Error Handling

Instructor has a built-in retry mechanism that is very useful when the LLM produces invalid output.

from instructor import retry

from tenacity import retry, stopafterattempt, waitfixed

Configure retry when creating the client

client = instructor.fromopenai(

OpenAI(),

maxretries=3, # Maximum 3 retries

)

Or configure retry per-request

class StrictOutput(BaseModel):

sentiment: str = Field(

description="Sentiment: positive, negative, or neutral"

)

confidence: float = Field(ge=0.0, le=1.0)

keyphrases: List[str] = Field(minlength=1, maxlength=5)

@fieldvalidator("sentiment")

@classmethod

def validatesentiment(cls, v: str) -> str:

allowed = {"positive", "negative", "neutral"}

if v.lower() not in allowed:

raise ValueError(

f"Sentiment must be one of: {allowed}"

)

return v.lower()

result = client.chat.completions.create(

model="gpt-4o-mini",

responsemodel=StrictOutput,

maxretries=5,

messages=[

{

"role": "user",

"content": "Analyze sentiment: 'This product is amazing, "

"fast delivery, but the packaging could be better'"

}

],

)

print(f"Sentiment: {result.sentiment}")

print(f"Confidence: {result.confidence}")

print(f"Key phrases: {result.keyphrases}")

When validation fails, Instructor automatically:

  • Catches the Pydantic validation error
  • Resends the request to the LLM with the error message as feedback
  • The LLM corrects the output based on the feedback
  • The process repeats until validation succeeds or max retries is reached
  • Streaming with Partial Objects

    For large objects, Instructor supports streaming so you can receive data incrementally.

    from instructor import Partial
    
    

    class Article(BaseModel):

    title: str

    summary: str

    keypoints: List[str]

    conclusion: str

    Streaming with Partial

    stream = client.chat.completions.create(

    model="gpt-4o-mini",

    responsemodel=Partial[Article],

    stream=True,

    messages=[

    {

    "role": "user",

    "content": "Write a short article about the benefits of AI "

    "in healthcare"

    }

    ],

    )

    for partialarticle in stream:

    # Each iteration gets a partially filled Article object

    if partialarticle.title:

    print(f"Title: {partialarticle.title}")

    if partialarticle.keypoints:

    print(f"Points so far: {len(partialarticle.keypoints)}")

    Practical Example: Data Extraction from Text

    Here is a real-world example for extracting product information from e-commerce descriptions.

    from pydantic import BaseModel, Field
    

    from typing import List, Optional

    class ProductSpec(BaseModel):

    key: str = Field(description="Specification name")

    value: str = Field(description="Specification value")

    class ExtractedProduct(BaseModel):

    name: str = Field(description="Product name")

    brand: str = Field(description="Product brand")

    priceusd: Optional[float] = Field(

    description="Price in USD", default=None

    )

    category: str = Field(description="Product category")

    specifications: List[ProductSpec] = Field(

    description="Technical specifications"

    )

    pros: List[str] = Field(description="Product advantages")

    cons: List[str] = Field(description="Product disadvantages")

    productdescription = """

    Samsung Galaxy S24 Ultra Review - Price $1,299

    Samsung delivers its best flagship yet. The Galaxy S24 Ultra features a

    6.8-inch Dynamic AMOLED 2X display, Snapdragon 8 Gen 3 processor,

    12GB RAM, and 256GB storage. The 200MP main camera produces incredibly

    detailed photos. The 5000mAh battery lasts all day.

    The pros are blazing fast performance, incredible camera, and the

    useful S Pen. The cons are the expensive price tag and the relatively

    heavy weight at 233 grams.

    """

    product = client.chat.completions.create(

    model="gpt-4o",

    responsemodel=ExtractedProduct,

    messages=[

    {

    "role": "system",

    "content": "Extract product information from the following review."

    },

    {"role": "user", "content": productdescription}

    ],

    )

    print(f"Product: {product.brand} {product.name}")

    print(f"Price: ${product.priceusd:,.2f}")

    print(f"Category: {product.category}")

    print("Specifications:")

    for spec in product.specifications:

    print(f" {spec.key}: {spec.value}")

    Practical Example: Text Classification

    Instructor is perfect for classification tasks because the output is guaranteed to match the defined categories.

    from enum import Enum
    

    from typing import List

    class TicketPriority(str, Enum):

    LOW = "low"

    MEDIUM = "medium"

    HIGH = "high"

    CRITICAL = "critical"

    class TicketCategory(str, Enum):

    BUG = "bug"

    FEATUREREQUEST = "featurerequest"

    QUESTION = "question"

    COMPLAINT = "complaint"

    BILLING = "billing"

    class ClassifiedTicket(BaseModel):

    priority: TicketPriority

    category: TicketCategory

    department: str = Field(

    description="Department responsible for handling"

    )

    summary: str = Field(

    description="One-sentence ticket summary", maxlength=200

    )

    suggestedresponse: str = Field(

    description="Suggested initial response for customer service"

    )

    requiresescalation: bool = Field(

    description="Whether escalation to a higher level is needed"

    )

    tickettext = """

    URGENT: Payment system has been down since yesterday. All transactions

    are failing and customers cannot checkout. We've lost approximately

    $50,000 in potential revenue. Please fix immediately!

    """

    classified = client.chat.completions.create(

    model="gpt-4o-mini",

    responsemodel=ClassifiedTicket,

    messages=[

    {

    "role": "system",

    "content": "Classify the following support ticket."

    },

    {"role": "user", "content": tickettext}

    ],

    )

    print(f"Priority: {classified.priority.value}")

    print(f"Category: {classified.category.value}")

    print(f"Department: {classified.department}")

    print(f"Escalation needed: {classified.requiresescalation}")

    print(f"Summary: {classified.summary}")

    Using with Other Providers

    Instructor is not limited to OpenAI. Here is how to use it with Anthropic.

    import instructor
    

    from anthropic import Anthropic

    With Anthropic Claude

    client = instructor.fromanthropic(Anthropic())

    class Analysis(BaseModel):

    topic: str

    mainarguments: List[str]

    conclusion: str

    result = client.messages.create(

    model="claude-sonnet-4-20250514",

    maxtokens=1024,

    responsemodel=Analysis,

    messages=[

    {

    "role": "user",

    "content": "Analyze the main arguments about the importance "

    "of AI regulation in Southeast Asia"

    }

    ],

    )

    Tips and Best Practices

    Here are some tips to maximize your use of Instructor:

    1. Use clear Field descriptions
    class GoodModel(BaseModel):
    

    # Good - clear description

    revenue: float = Field(

    description="Total revenue in USD millions"

    )

    # Not ideal - no description

    revenue: float

    2. Choose the right model for the right task
    • Use gpt-4o-mini for simple tasks (classification, basic extraction)
    • Use gpt-4o for complex tasks (nested objects, reasoning)

    3. Leverage system prompts
    messages=[
    

    {

    "role": "system",

    "content": "You are a data extraction assistant. "

    "Always provide output in English. "

    "If information is not available, use null."

    },

    {"role": "user", "content": text}

    ]

    4. Handle errors gracefully
    from instructor.exceptions import InstructorRetryException
    
    

    try:

    result = client.chat.completions.create(

    model="gpt-4o-mini",

    responsemodel=MyModel,

    max_retries=3,

    messages=[{"role": "user", "content": text}],

    )

    except InstructorRetryException as e:

    print(f"Failed after retries: {e}")

    except Exception as e:

    print(f"Error: {e}")

    Conclusion

    Instructor is an incredibly useful library for anyone working with LLMs who needs structured output. By leveraging Pydantic, Instructor provides type safety, automatic validation, and retry logic that makes integrating LLMs into production applications far more reliable.

    Key features we covered in this tutorial:

    • Basic usage with OpenAI and other providers
    • Nested Pydantic models for complex data
    • Custom validation with field and model validators
    • Automatic retry logic when validation fails
    • Streaming with Partial objects
    • Practical examples for data extraction and classification

    Start with simple use cases like data extraction or classification, then gradually increase complexity as your project requires. Instructor will become an invaluable tool in your AI engineering toolkit.

    Related Articles

    Complete Instructor Tutorial: Structured Outputs from LLMs with Pydantic

    Tutorial Lengkap Instructor: Output Terstruktur dari LLM dengan Pydantic Halo temen-temen, di tutorial ini aku mau ngaja...

    PydanticAI Tutorial: A Type-Safe Agent Framework for LLM Apps

    Membangun Agen LLM yang Type-Safe dengan PydanticAI PydanticAI adalah framework agen dari tim di balik Pydantic, diranca...

    Complete Guidance Tutorial: Constrained Generation and Structured Output from LLMs

    Tutorial Lengkap Guidance: Constrained Generation dan Structured Output dari LLM Halo temen-temen, di tutorial kali ini ...

    Mirascope Tutorial: A Pythonic Toolkit for Building LLM Applications

    Tutorial Mirascope: Toolkit Pythonic untuk Membangun Aplikasi LLM Pendahuluan Mirascope adalah library Python yang menye...