Pydantic and OpenAI: Revolutionizing Structured Outputs of LLMs

Le brief IA que les pros lisent chaque soir
Les 7 actus IA du jour, décryptées en 5 min. Gratuit.
Inclus dès l'inscription : notre sélection des meilleurs guides & comparatifs IA.
Choisis ton rythme
Gratuit · Pas de spam · Désabonnement en 1 clic
Applications of LLMs
In the field of language models, obtaining structured and machine-readable responses is essential for many applications. Three main approaches stand out to achieve this goal: JSON Mode, Function Calling, and OpenAI's Structured Outputs. These methods allow for structuring the data returned by language models, thus facilitating their integration into automated systems.
However, a new dimension is added with the introduction of Pydantic. This Python library, while distinct from OpenAI's functionalities, proves to be a powerful complement. It allows for processing the JSON returned by the models, ensuring data type validation, field access, and handling unexpected values. Thus, Pydantic positions itself as an indispensable tool for those looking to build reliable and robust applications based on LLMs.
In my opinion, the combination of Pydantic and OpenAI's Structured Outputs constitutes the cleanest setup currently available for building reliable LLM-powered applications in Python. By the end of this article, you will understand exactly why.
What is Pydantic?
Pydantic is a library that focuses on data validation using type annotations in Python. It allows you to define the structure and types of data in the form of Python classes and verifies that the provided data complies with these definitions. In case of non-compliance, Pydantic generates clear errors, thus avoiding the propagation of incorrect data within the system.
Let's take a simple example:
from pydantic import BaseModel
class PersonInfo(BaseModel):
name: str
age: int
city: str
This model enforces that name and city must be strings, while age must be an integer. If an attempt is made to create a PersonInfo with an age as a string, Pydantic will immediately detect it and report the error.
Unlike previous methods such as JSON Mode or Function Calling, which may return schema non-compliant responses, Pydantic ensures that the data is always compliant and valid. This is particularly useful when working with OpenAI's Structured Outputs, as it guarantees that the returned JSON adheres to the defined schema while being processed more efficiently and securely in Python.
With Pydantic, we define our schema as a Python class, and integration with the OpenAI API means we get a true Python object, not a dictionary, with all fields typed and validated automatically. The JSON Schema that the API needs is generated from our Pydantic model in the background. We never have to write it ourselves. In other words, Pydantic is the schema definition layer that sits between our Python code and the OpenAI API, making the entire structured output experience cleaner, safer, and more maintainable.
Practical Comparison of Pydantic with Structured Outputs
Let's see how the integration of Pydantic enhances the use of Structured Outputs. Before Pydantic, obtaining a structured output involved defining the schema as a raw dictionary and manually parsing the results.
from openai import OpenAI
client = OpenAI(api_key="your_api_key")
schema = {
"type": "function",
"name": "extract_person_info",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"},
"city": {"type": "string"}
},
"required": ["name", "age", "city"],
"additionalProperties": False
}
response = client.chat.completions.create(
model="[gpt](/glossaire/gpt)-4o-mini",
tool_choice={"type": "function", "function": {"name": "extract_person_info"}},
messages=[{"role": "user", "content": "Extract info from: 'Maria is 32 years old and lives in Athens.'"}]
)
result = json.loads(response.choices[0].message.tool_calls[0].function.arguments)
print(result["name"]) # "Maria"
print(result["age"]) # could be "32" instead of 32
With Pydantic, this process is simplified and secured:
from openai import OpenAI
from pydantic import BaseModel
client = OpenAI(api_key="your_api_key")
class PersonInfo(BaseModel):
name: str
age: int
city: str
response = client.beta.chat.completions.parse(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "Extract info from: 'Maria is 32 years old and lives in Athens.'"}],
response_format=PersonInfo
)
result = response.choices[0].message.parsed
print(result.name) # "Maria"
print(result.age) # 32
print(result.city) # "Athens"
The version using Pydantic is not only more concise, but it also ensures that the data is correctly typed and validated. By passing our Pydantic model to response_format, the OpenAI SDK automatically generates the JSON Schema and parses the response into a complete Python object.
Additionally, note the method .parse(), instead of the usual .create() method. This is because .parse() is a method of the OpenAI SDK specifically designed to work with Pydantic models, managing the entire structured output pipeline from end to end.
Building with Pydantic
1. Nested Models and Lists
Pydantic excels at handling nested data structures. Defining JSON schemas for nested objects can be tedious, but with Pydantic, you simply create Python classes:
from pydantic import BaseModel
from typing import List
class Address(BaseModel):
city: str
class ContactInfo(BaseModel):
address: Address
phone_numbers: List[str]
response = client.beta.chat.completions.parse(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "Extract contact info from: 'Maria Mouschoutzi, [email protected], Ermou 15, Athens, Greece. Phone: +30 210 1234567, +30 697 8901234'"}],
response_format=ContactInfo
)
contact = response.choices[0].message.parsed
print(contact.name) # "Maria Mouschoutzi"
print(contact.address.city) # "Athens"
print(contact.phone_numbers[0]) # "+30 210 1234567"
With Pydantic, nested models are automatically validated, and accessing fields is intuitive. This significantly reduces the risk of errors such as KeyError.
2. Validation with Pydantic Fields
Pydantic also offers advanced data validation features. By using Field, you can add explicit constraints on acceptable values.
from pydantic import BaseModel, Field
from typing import Optional
class ProductReview(BaseModel):
product_name: str = Field(description="The name of the reviewed product")
rating: int = Field(ge=1, le=5, description="Rating from 1 to 5 stars")
sentiment: str = Field(description="One of: positive, neutral, negative")
summary: str = Field(max_length=200, description="A brief summary of the review")
verified_purchase: Optional[bool] = Field(default=None, description="Whether it is a verified purchase")
response = client.beta.chat.completions.parse(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "Extract a structured review from: 'Absolutely love this coffee machine! Makes perfect espresso every time. 5 stars, would recommend to anyone.'"}],
response_format=ProductReview
)
review = response.choices[0].message.parsed
print(review.product_name) # "coffee machine"
print(review.rating) # 5
print(review.sentiment) # "positive"
print(review.summary) # "Makes perfect espresso every time."
The constraints on the rating field ensure that only ratings between 1 and 5 are accepted. Additionally, the field descriptions serve as guidance for the model, thereby improving the accuracy of the returned data.
Finally, in the event of a model refusal, the OpenAI SDK handles this elegantly, providing information on the reason for the refusal. This is crucial for production applications, where transparency and error management are essential.
Thus, here’s how a model refusal with Pydantic would unfold:
response = client.beta.chat.completions.parse(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "Extract info from the job posting above."}],
response_format=JobPosting
)
message = response.choices[0].message
if message.refusal:
print(f"Model refused: {message.refusal}")
job = message.parsed
print(f"Extracted: {job.job_title} at {job.company_name}")
Brief IA — L'actualité IA en français
L'essentiel de l'actualité de l'intelligence artificielle, décrypté et expliqué chaque jour.