Using Pydantic and Instructor for Reliable Structured JSON Output in Python
I've spent the last month migrating a complex data extraction pipeline from raw LangChain calls to a combination of Instructor and Pydantic, and the difference in reliability is night and day. The core problem with standard LLM calls is that they are probabilistic; Instructor turns them into a deterministic function call by leveraging Pydantic for schema validation and automatic retries.
Here is how I've set up my current workflow to ensure the LLM output actually matches my TypeScript frontend expectations.
First, define your data model using Pydantic. The trick here is to use Field descriptions. Instructor passes these descriptions directly into the JSON schema sent to the LLM, which acts as a much more powerful guide than a giant wall of text in the system prompt.
from pydantic import BaseModel, Field
from typing import List, Optional
import instructor
from openai import OpenAI
class UserInsight(BaseModel):
sentiment: str = Field(description="Sentiment of the user: Positive, Neutral, or Negative")
key_pain_points: List[str] = Field(description="List of specific frustrations mentioned")
urgency_score: int = Field(description="Score from 1-10 based on how urgent the request is")
follow_up_required: bool
class AnalysisResponse(BaseModel):
insights: List[UserInsight]
summary: str = Field(description="A one-sentence summary of the overall mood")Then, you patch your OpenAI client. This is the "magic" part—Instructor wraps the client so that response_model becomes a first-class citizen.
client = instructor.from_openai(OpenAI())
# This will automatically retry if the LLM returns invalid JSON
# or fails the Pydantic validation
data = client.chat.completions.create(
model="gpt-4o",
response_model=AnalysisResponse,
messages=[
{"role": "user", "content": "The user says: I love the UI but the API is slow and I'm losing money! Fix it now!"}
],
max_retries=3
)
print(data.summary)
# Output: User is frustrated with API performance despite liking the UI.The productivity gains I've seen:
- Type Safety: I get full IDE autocomplete for the LLM response. No more
response['data'][0]['sentiment']and praying the key exists. - Automatic Validation: If the LLM returns a string for
urgency_scoreinstead of an int, Instructor catches the PydanticValidationError, sends the error back to the LLM, and asks it to fix it—all in one call. - Schema Evolution: Changing the output format is as simple as adding a field to the Pydantic class.
A few gotchas to watch out for:
Token Overhead. Because Instructor sends the full JSON schema in the request, your input token count increases. For massive schemas, this can get expensive or hit context limits on smaller models.
Model Selection. While GPT-4o handles this flawlessly, some smaller open-source models struggle with complex nested Pydantic models. If you're using Local LLMs via Ollama, keep your schemas flat.
The "Retry Loop" Trap. If your Pydantic validation is too strict (e.g., using a very narrow Literal or a complex validator), the LLM might get stuck in a retry loop where it keeps making the same mistake. If you see 3/3 retries failing, loosen the validation or improve the Field description.
All Replies (0)
No replies yet — be the first!
