In my newest put up on structured outputs, the three predominant approaches for getting machine-readable responses from an LLM. These are JSON Mode, Perform Calling, and OpenAI’s Structured Outputs. In the event you haven’t learn that put up but, it’s price a fast learn earlier than this one, since we’ll be constructing immediately on high of it.
So, at the moment, we’ll be going one step additional and speak about one thing that modifications the best way structured outputs really feel in follow. That’s Pydantic. Extra particularly, whereas OpenAI’s Structured Outputs characteristic ensures that the mannequin returns legitimate, schema-conforming JSON, we nonetheless have to do stuff with that JSON on the Python aspect. Particularly, we have to parse it, validate the information varieties, entry the fields, deal with any surprising values, and so forth. And that’s the place Pydantic is available in.
For my part, the mix of Pydantic and OpenAI’s Structured Outputs is the cleanest setup obtainable proper now for constructing dependable LLM-powered purposes in Python. By the tip of this put up, you’ll see precisely why.
what about Pydantic?
Pydantic is a Python library for information validation utilizing kind annotations. Which means it permits you to outline the form and forms of your information as a Python class, after which validates that any information you go in really conforms to that description. If it doesn’t, Pydantic raises a transparent, descriptive error relatively than letting dangerous information silently propagate by your system.
Right here’s the best doable Pydantic mannequin:
from pydantic import BaseModel
class PersonInfo(BaseModel):
identify: str
age: int
metropolis: str
On this method, we now have a schema that enforces that identify and metropolis are at all times going to be a string, whereasage is at all times an integer. If somebody tries to create a PersonInfo with age="thirty-two" Pydantic will catch it instantly and inform us precisely what went flawed.
However isn’t this simply what we had been already doing with Perform Calling and Structured Outputs? Sure, however with a vital distinction.
On one aspect, with JSON Mode or Perform Calling, the mannequin might or might not return a response that matches the schema we had in thoughts. Even when it does get the schema proper, it nonetheless returns a plain string of JSON, leaving us to manually parse fields, solid varieties, and validate values on the Python aspect.
On the opposite aspect, Structured Outputs improves on this by guaranteeing that the returned JSON at all times conforms to our outlined schema, due to constrained decoding on the mannequin degree. Nonetheless, it’s nonetheless only a string of JSON that we have to deal with ourselves in Python.
With Pydantic, we outline our schema as a Python class, and the combination with OpenAI’s API means we get again a correct Python object, not a dictionary, with all fields typed and validated robotically. The JSON Schema that the API wants is generated from our Pydantic mannequin behind the scenes. We by no means have to write down it ourselves. In different phrases, Pydantic is the schema definition layer that sits between our Python code and OpenAI’s API, making the entire structured output expertise cleaner, safer, and extra maintainable.
🍨 DataCream is a e-newsletter on AI, information, and tech. If you’re considering these matters, subscribe right here!
However let’s see in follow what we acquire from utilizing Pydantic compared to simply utilizing Structured Outputs.

Right here’s what getting structured output appeared like earlier than Pydantic integration:
from openai import OpenAI
import json
consumer = OpenAI(api_key="your_api_key")
# outline schema as a uncooked dictionary
instruments = [
{
"type": "function",
"function": {
"name": "extract_person_info",
"strict": True,
"parameters": {
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"},
"city": {"type": "string"}
},
"required": ["name", "age", "city"],
"additionalProperties": False
}
}
}
]
response = consumer.chat.completions.create(
mannequin="gpt-4o-mini",
instruments=instruments,
tool_choice={"kind": "operate", "operate": {"identify": "extract_person_info"}},
messages=[
{"role": "user", "content": "Extract info from: 'Maria is 32 years old and lives in Athens.'"}
]
)
# parse manually — we get again a plain dictionary
outcome = json.masses(response.decisions[0].message.tool_calls[0].operate.arguments)
print(outcome["name"]) # "Maria" — however what if the bottom line is lacking? KeyError.
print(outcome["age"]) # may be "32" as a substitute of 32 — kind not assured
And right here’s the identical factor with Pydantic:
from openai import OpenAI
from pydantic import BaseModel
consumer = OpenAI(api_key="your_api_key")
class PersonInfo(BaseModel):
identify: str
age: int
metropolis: str
response = consumer.beta.chat.completions.parse(
mannequin="gpt-4o-mini",
messages=[
{"role": "user", "content": "Extract info from: 'Maria is 32 years old and lives in Athens.'"}
],
response_format=PersonInfo
)
# we get again a correct Python object, absolutely typed and validated
outcome = response.decisions[0].message.parsed
print(outcome.identify) # "Maria" — at all times a string
print(outcome.age) # 32 — at all times an integer, by no means a string
print(outcome.metropolis) # "Athens" — at all times a string
Clearly, the Pydantic model is shorter, extra readable, and safer. Discover how we go our Pydantic mannequin to response_format, and the OpenAI SDK, which handles producing the JSON Schema, simply makes the API name with strict: True, and parses the response again into a correct PersonInfo object. On this method, we get kind security, validation, and dot-notation entry to fields.
On high of this, additionally discover the .parse() technique, as a substitute of the standard .create(). It is because .parse() is a technique on the OpenAI SDK particularly designed to work with Pydantic fashions, dealing with the total structured output pipeline finish to finish.
build up with Pydantic
1. nested fashions and lists
One of many locations the place Pydantic actually shines is nested information constructions. Uncooked JSON Schema for nested objects is painful to write down, however when utilizing Pydantic, it’s simply defining Python courses:
from pydantic import BaseModel
from typing import Record
class Handle(BaseModel):
avenue: str
metropolis: str
nation: str
class ContactInfo(BaseModel):
identify: str
e mail: str
deal with: Handle
phone_numbers: Record[str]
response = consumer.beta.chat.completions.parse(
mannequin="gpt-4o-mini",
messages=[
{
"role": "user",
"content": """Extract contact info from:
'Maria Mouschoutzi, [email protected],
Ermou 15, Athens, Greece.
Telephone: +30 210 1234567, +30 697 8901234'"""
}
],
response_format=ContactInfo
)
contact = response.decisions[0].message.parsed
print(contact.identify) # "Maria Mouschoutzi"
print(contact.deal with.metropolis) # "Athens"
print(contact.phone_numbers[0]) # "+30 210 1234567"
The Handle mannequin is nested inside ContactInfo naturally, and Pydantic handles the total validation of the nested construction. We will then entry fields with clear dot notation, as as an illustration, contact.deal with.metropolis relatively than outcome["address"]["city"], which might elevate KeyError if any intermediate secret is lacking. So, one of many duties the place Pydantic comes actually useful is when needing nested information constructions.
2. validation with Pydantic discipline
One other activity on which Pydantic may be very helpful is the validation of the returned information. This validation contains checking information varieties, but in addition goes past this, since Pydantic additionally permits imposing clear directions on the precise contents of the returned values. Extra particularly, we are able to add specific validation constraints utilizing Discipline, giving the mannequin directions about what values are acceptable and what not.
For instance, let’s assume we have now a setup returning information about merchandise critiques:
from pydantic import BaseModel, Discipline
from typing import Non-compulsory
class ProductReview(BaseModel):
product_name: str = Discipline(description="The identify of the product being reviewed")
ranking: int = Discipline(ge=1, le=5, description="Score from 1 to five stars")
sentiment: str = Discipline(description="Considered one of: optimistic, impartial, destructive")
abstract: str = Discipline(max_length=200, description="A short abstract of the overview")
verified_purchase: Non-compulsory[bool] = Discipline(default=None, description="Whether or not it is a verified buy")
response = consumer.beta.chat.completions.parse(
mannequin="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
)
overview = response.decisions[0].message.parsed
print(overview.product_name) # "espresso machine"
print(overview.ranking) # 5
print(overview.sentiment) # "optimistic"
print(overview.abstract) # "Makes excellent espresso each time."
Particularly, the ge=1, le=5 on the ranking discipline tells the mannequin that legitimate scores are between 1 and 5. The max_length=200 on the abstract ensures we by no means get a response longer than 200 characters.
As well as, the description fields act as directions to the mannequin, serving to it perceive precisely what every discipline ought to include. That is primarily the equal of the description fields we wrote manually in Perform Calling schemas, guiding the mannequin on what to place in every discipline.
3. refusals
One other factor price highlighting is that by utilizing .parse(), the OpenAI SDK additionally handles mannequin refusals gracefully. Extra particularly, within the case that the mannequin refuses to finish a request (for instance, as a result of the content material violates the mannequin’s insurance policies), the parsed discipline shall be None and the refusal discipline will include the explanation. This permits us to get a structured outcome, even within the case of refusal, a minimum of informing the applying person of the explanation why their request was denied. That is of explicit significance for AI-powered apps operating in manufacturing, the place we are able to’t actually predict what unusual factor the person might request.
So, that is how a mannequin refusal with Pydantic would play out:
response = consumer.beta.chat.completions.parse(
mannequin="gpt-4o-mini",
messages=[{"role": "user", "content": "Extract info from the job posting above."}],
response_format=JobPosting
)
message = response.decisions[0].message
if message.refusal:
print(f"Mannequin refused: {message.refusal}")
else:
job = message.parsed
print(f"Extracted: {job.job_title} at {job.company_name}")
An actual-world instance: doc data extraction
Let’s put every thing along with a extra life like instance. Think about we’re constructing a pipeline that processes job postings and extracts structured data from them. That is precisely the type of activity the place structured outputs shine: unstructured enter, well-defined output schema, and a downstream system that should insert the outcome right into a database.
from pydantic import BaseModel, Discipline
from typing import Record, Non-compulsory
from openai import OpenAI
consumer = OpenAI(api_key="your_api_key")
class SalaryRange(BaseModel):
min_salary: Non-compulsory[int] = Discipline(default=None, description="Minimal wage in USD per 12 months")
max_salary: Non-compulsory[int] = Discipline(default=None, description="Most wage in USD per 12 months")
foreign money: str = Discipline(default="USD", description="Forex code")
class JobPosting(BaseModel):
job_title: str = Discipline(description="The job title or function identify")
company_name: str = Discipline(description="The identify of the hiring firm")
location: str = Discipline(description="Job location, e.g. 'Athens, Greece' or 'Distant'")
employment_type: str = Discipline(description="Considered one of: full-time, part-time, contract, freelance")
required_skills: Record[str] = Discipline(description="Record of required technical abilities")
years_of_experience: Non-compulsory[int] = Discipline(default=None, description="Minimal years of expertise required")
wage: Non-compulsory[SalaryRange] = Discipline(default=None, description="Wage vary if talked about")
remote_friendly: bool = Discipline(description="Whether or not distant work is allowed")
job_post_text = """
Senior Python Engineer at pialgorithms (Athens, Greece / Distant)
We're on the lookout for an skilled Python developer to hitch our AI group.
You'll work on our doc administration platform, constructing clever
search and extraction options utilizing LLMs and RAG pipelines.
Necessities:
- 5+ years of Python expertise
- Sturdy data of FastAPI, LangChain, and vector databases
- Expertise with OpenAI API and Pydantic
- Familiarity with Docker and cloud deployment (AWS/GCP)
Wage: €60,000 - €85,000 per 12 months
Full-time place. Distant-friendly.
"""
response = consumer.beta.chat.completions.parse(
mannequin="gpt-4o-mini",
messages=[
{
"role": "system",
"content": "You are an expert at extracting structured information from job postings."
},
{
"role": "user",
"content": f"Extract all relevant information from this job posting:nn{job_post_text}"
}
],
response_format=JobPosting
)
job = response.decisions[0].message.parsed
print(f"Title: {job.job_title}")
print(f"Firm: {job.company_name}")
print(f"Location: {job.location}")
print(f"Distant: {job.remote_friendly}")
print(f"Expertise: {', '.be part of(job.required_skills)}")
print(f"Expertise: {job.years_of_experience} years")
if job.wage:
print(f"Wage: {job.wage.min_salary} - {job.wage.max_salary} {job.wage.foreign money}")
And the output appears one thing like this:
Title: Senior Python Engineer
Firm: pialgorithms
Location: Athens, Greece / Distant
Distant: True
Expertise: Python, FastAPI, LangChain, vector databases, OpenAI API, Pydantic, Docker, AWS, GCP
Expertise: 5 years
Wage: 60000 - 85000 EUR
That is production-ready extraction code. The nested SalaryRange mannequin handles the non-compulsory wage data cleanly, Non-compulsory fields default to None gracefully when data is lacking, and each discipline within the response is typed and validated robotically. The outcome might be inserted immediately right into a database or handed to the following step in a pipeline with none further processing.
On my thoughts
What I discover most satisfying in regards to the Pydantic + OpenAI mixture is how nicely it matches the best way a Python developer already thinks. It defines information constructions as courses, makes use of kind hints, and catches kind errors early and loudly. All these are historically essentially the most unpredictable elements of any AI utility, principally as a result of we needed to do all of those ourselves manually on the Python aspect of the AI app. Pydantic works as a greater, extra sturdy connection between the AI mannequin and the remainder of our Python code.
✨ Thanks for studying! ✨
In the event you made it this far, you may discover pialgorithms helpful: a platform we’ve been constructing that helps groups securely handle organisational data in a single place.
Liked this put up? Be part of me on 💌 Substack and 💼 LinkedIn
All photographs by the creator, besides the place talked about in any other case.















