Brief IA

Pydantic and OpenAI: Revolutionizing Structured Outputs of LLMs

🔬 Research·Tom Levy·

Pydantic and OpenAI: Revolutionizing Structured Outputs of LLMs

Pydantic and OpenAI: Revolutionizing Structured Outputs of LLMs
Key Takeaways
1The integration of Pydantic with OpenAI enhances the management of structured outputs from language models.
2Pydantic allows for automatic data validation, ensuring responses conform to defined schemas.
3The use of Pydantic simplifies the handling of nested data and strengthens application security.
💡Why it mattersThis combination provides a robust solution for developing reliable and maintainable AI applications, which is crucial for businesses using LLMs.
Le brief IA que lisent les pros

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

📄
Full Analysis

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.