Pydantic + OpenAI: The Cleanest Way to Get Structured Outputs from LLMs
Learn how to combine Pydantic models with OpenAI's API to reliably extract structured, validated data from LLM responses—eliminating parsing errors and ensuring type safety in your AI applications.
Tags
Quick summary
Learn how to combine Pydantic models with OpenAI's API to reliably extract structured, validated data from LLM responses—eliminating parsing errors and ensuring type safety in your AI applications.
Pydantic + OpenAI: The Cleanest Way to Get Structured Outputs from LLMs
Large Language Models (LLMs) are remarkably good at generating human-like text, but they often produce unstructured or semi-structured outputs that require extensive post-processing. When building production applications—whether for data extraction, content moderation, or automated reporting—you need reliable, typed, and validated data. Enter Pydantic and OpenAI: a powerful combination that lets you define exactly the output structure you want, and have the LLM deliver it in a clean, predictable format.
This article walks through the practical steps to set up Pydantic with OpenAI, from installation to real-world usage. By the end, you’ll have a reusable pattern for obtaining structured outputs from GPT models with minimal boilerplate.
Why Structured Outputs Matter
When you ask an LLM a question, you typically get back a paragraph of text. For a chatbot, that’s fine. But if you need to extract a list of customer names, dates, and amounts from an email, or generate a JSON object for a database, raw text becomes a problem. You might try to parse the output with regex or a second LLM call, but these approaches are brittle and error-prone.
Structured outputs solve this by constraining the model to produce data that matches a predefined schema. This is where Pydantic shines. Pydantic is a Python library for data validation using Python type annotations. When combined with OpenAI’s function calling or structured output capabilities, you get:
- **Type safety** – The output is automatically validated against your schema.
- **No manual parsing** – The model returns a Pydantic model instance, not raw text.
- **Clear contracts** – Your code knows exactly what fields to expect.
Requirements
Before diving in, ensure you have the following:
- **Python 3.9+** – Pydantic v2 requires Python 3.9 or later.
- **An OpenAI API key** – You need a valid key with access to GPT-4 or GPT-3.5-turbo models that support function calling.
- **Basic familiarity with Python** – You should understand classes, types, and async programming (optional but helpful).
Step-by-Step Installation
We’ll set up a clean environment and install the necessary libraries.
1. Create and activate a virtual environment
Isolating dependencies prevents conflicts with other projects.
python -m venv pydantic_openai_env
source pydantic_openai_env/bin/activate # On Windows: pydantic_openai_env\Scripts\activate2. Install Pydantic and the OpenAI Python library
Pydantic v2 is the current major version. The OpenAI library includes support for function calling and structured outputs.
pip install pydantic openai3. Verify the installation
Run a quick check to confirm both libraries are available.
import pydantic
import openai
print(f"Pydantic version: {pydantic.__version__}")
print(f"OpenAI library version: {openai.__version__}")If you see version numbers (e.g., `2.5.0` for Pydantic and `1.6.0` for OpenAI), you’re ready.
4. Set your OpenAI API key
Store your key as an environment variable for security.
export OPENAI_API_KEY="your-api-key-here"You can also set it in your Python script, but using environment variables is recommended for production.
Core Concept: Defining a Pydantic Model
A Pydantic model is a class that inherits from `BaseModel`. Each field is annotated with a type, and optional default values or validators can be added.
Here’s a simple model for extracting a person’s information from text:
from pydantic import BaseModel, Field
from typing import Optional
class Person(BaseModel):
name: str = Field(description="The full name of the person")
age: Optional[int] = Field(None, description="Age in years, if mentioned")
email: Optional[str] = Field(None, description="Email address, if present")The `Field` function lets you add descriptions, default values, and constraints. These descriptions are passed to the LLM to guide its output.
The Magic: Combining Pydantic with OpenAI Function Calling
OpenAI’s function calling allows you to specify a JSON schema that the model must adhere to when generating a response. Pydantic models can be automatically converted to this schema using the `.model_json_schema()` method.
Here’s a complete workflow:
Step 1: Define your output model
We’ll create a model for extracting a list of product reviews.
from pydantic import BaseModel, Field
from typing import List
class Review(BaseModel):
product_name: str = Field(description="Name of the reviewed product")
rating: int = Field(ge=1, le=5, description="Rating from 1 to 5")
summary: str = Field(description="One-sentence summary of the review")
class ReviewList(BaseModel):
reviews: List[Review] = Field(description="List of reviews found in the text")Step 2: Prepare the OpenAI client and function definition
We’ll use the OpenAI client (v1.0+ syntax) and convert our model to a JSON schema.
from openai import OpenAI
client = OpenAI()
# Convert ReviewList to a JSON schema
review_schema = ReviewList.model_json_schema()The schema will look like a standard JSON Schema object, which OpenAI uses to define the function parameters.
Step 3: Make the API call with function calling
We pass the schema as a function definition, and ask the model to call that function.
response = client.chat.completions.create(
model="gpt-4-1106-preview", # or "gpt-3.5-turbo-1106"
messages=[
{"role": "system", "content": "You extract structured data from user input."},
{"role": "user", "content": "I loved the new iPhone 15. The camera is amazing. I give it 5 stars. Also, the MacBook Pro is overpriced but powerful. I'd rate it 3 stars."}
],
tools=[{
"type": "function",
"function": {
"name": "extract_reviews",
"description": "Extract product reviews from the input text",
"parameters": review_schema
}
}],
tool_choice={"type": "function", "function": {"name": "extract_reviews"}}
)Step 4: Parse the response into a Pydantic model
The model’s response will contain a function call argument (a JSON string). We parse it directly into our Pydantic model.
import json
# Extract the function call arguments
tool_call = response.choices[0].message.tool_calls[0]
arguments = json.loads(tool_call.function.arguments)
# Validate and create the Pydantic model
reviews = ReviewList(**arguments)
print(reviews)Output (formatted):
reviews=[
Review(product_name='iPhone 15', rating=5, summary='Loved the amazing camera.'),
Review(product_name='MacBook Pro', rating=3, summary='Overpriced but powerful.')
]Notice that the model automatically validated the rating range (1–5) and parsed the list. If the LLM had returned an invalid rating (e.g., 6), Pydantic would raise a `ValidationError`.
Usage Examples
Let’s explore two practical scenarios: extracting structured data from emails and generating configuration objects.
Example 1: Email Parsing for Customer Support
Imagine you receive support emails and need to extract the issue type, priority, and customer ID.
from pydantic import BaseModel, Field
from enum import Enum
class Priority(str, Enum):
low = "low"
medium = "medium"
high = "high"
class SupportTicket(BaseModel):
customer_id: str = Field(description="Customer ID from the email signature")
issue_type: str = Field(description="Category of the issue: billing, technical, or account")
priority: Priority = Field(description="Urgency level")
description: str = Field(description="Brief summary of the problem")Then, use the same pattern as above to extract this from an email body. The LLM will return a validated `SupportTicket` instance.
Example 2: Generating Configuration Objects
You can also generate complex nested configs, such as database connection parameters.
from pydantic import BaseModel, Field
from typing import Dict
class DatabaseConfig(BaseModel):
host: str = Field(description="Database hostname or IP")
port: int = Field(ge=1, le=65535, description="Port number")
credentials: Dict[str, str] = Field(description="Username and password as a dict")Ask the LLM: “Generate a database config for a PostgreSQL instance running on localhost with user ‘admin’ and password ‘secret123’.” The model will return a validated config.
Best Practices and Tips
1. **Use descriptive field names and descriptions** – The LLM uses these to understand what you want. Be specific. 2. **Leverage type constraints** – Pydantic supports `ge`, `le`, `min_length`, `regex`, and more. These constraints are encoded in the JSON schema and help the LLM generate valid outputs. 3. **Handle errors gracefully** – Pydantic will raise `ValidationError` if the model output doesn’t match your schema. Wrap the parsing in a try-except block and retry with a clearer prompt. 4. **Keep schemas reasonably sized** – Very large schemas (many fields or deep nesting) can confuse the model. Break complex extractions into multiple calls if needed. 5. **Use enums for fixed sets** – If a field can only be one of a few values, define an `Enum` class. This constrains the LLM’s output to valid options.
Limitations and Considerations
While this approach is powerful, it’s not perfect:
- **Cost** – Function calling uses more tokens, especially with large schemas. Monitor your API usage.
- **Model support** – Not all OpenAI models support function calling. Use `gpt-4-1106-preview` or `gpt-3.5-turbo-1106` for best results.
- **Schema complexity** – Extremely nested or recursive schemas may cause the model to hallucinate or omit fields. Keep it flat when possible.
- **Latency** – The additional processing for function calling adds a small overhead (typically <100ms).
Conclusion
Pydantic + OpenAI provides the cleanest, most reliable way to get structured outputs from LLMs. By defining a Pydantic model, converting it to a JSON schema, and using OpenAI’s function calling, you eliminate manual parsing and gain type safety, validation, and clear contracts. This pattern is ideal for data extraction, form filling, code generation, and any task where you need predictable, structured data from natural language.
Start small: define a simple model for your next project, integrate it with a single API call, and iterate. As you become comfortable, you can scale to complex multi-step workflows. The combination of Pydantic’s validation power and OpenAI’s language understanding is a game-changer for building robust AI applications.
*Note: This article is based on general knowledge of Pydantic and OpenAI as of early 2025. For the latest updates, refer to the official Pydantic documentation and OpenAI’s API changelog.*
Sources
FAQ
What is this article about?
This article covers “Pydantic + OpenAI: The Cleanest Way to Get Structured Outputs from LLMs” in the AI tools category. Learn how to combine Pydantic models with OpenAI's API to reliably extract structured, validated data from LLM responses—eliminating parsing errors and ensuring type safety in your AI applications.
Who is this useful for?
It is useful for readers who want a practical understanding of AI tools, models, and workflows.
What should I do next?
Read the article, review the listed sources, and test the most relevant ideas in your own workflow.



