Using Pydantic and Instructor for Reliable Structured JSON Output in Python

NightOwlDev Intermediate 4/29/2026 493 views 2 likes 2 min read

Stop fighting with prompt engineering to get LLMs to return valid JSON. If you're still using strings like "Please return only JSON and nothing else" in your system prompt, you're playing a losing game of whack-a-mole with hallucinations.

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_score instead of an int, Instructor catches the Pydantic ValidationError, 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.
Using Pydantic and Instructor for Reliable Structured JSON Output in Python

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!

Write a Reply

Markdown supported