For this exercise, your task is to turn free-text resumes (like the one shown below) into a Python object using Structured Outputs.
Starter code:
from openai import OpenAI
from pydantic import BaseModel, Field
from typing import Literal
from dotenv import load_dotenv, find_dotenv
_ = load_dotenv(find_dotenv())
client = OpenAI()
resume = """
Maria Lopez
Email: maria.lopez@example.com | Phone: +34 600 123 456
Summary: Data analyst with 5 years of experience turning raw data into
business insights.
Skills: Python, SQL, Excel, Tableau, pandas
Experience:
- Data Analyst at Acme Corp (2021-2024)
- Junior Analyst at DataWorks (2019-2021)
Education: BSc in Statistics, University of Valencia
"""- Create a Pydantic model called
Candidatewith these fields:name(string)email(string)skills(list of strings)
- Call
client.responses.parse()with the resume as input andCandidateas thetext_format. - Print
name,email, andskillsfromresponse.output_parsed.
- Create a second model called
Jobwith:company,title,start_year(int),end_year(int). - Add a field
jobstoCandidate. It should be a list ofJob. - Run the call again and print each job in a loop, for example
Data Analyst @ Acme Corp (2021-2024).
Add a seniority field to Candidate that can only be "junior", "mid", or "senior". Add a Field description so the model knows how to choose (for example, based on years of experience).
Parse this resume, which has no email:
resume_2 = """
Carlos Ruiz
Backend developer. Skills: Java, Docker, PostgreSQL.
Worked at TechNova from 2020 to 2023 as Software Engineer.
"""What does the model return for email? Change the schema so email can be missing (None) instead of the model inventing a value. Hint: use str | None.
Solution: Iteration 1
class Candidate(BaseModel):
name: str
email: str
skills: list[str]
response = client.responses.parse(
model="gpt-6-luna",
input=[
{"role": "system", "content": "Extract the candidate information from the resume."},
{"role": "user", "content": resume},
],
text_format=Candidate,
)
candidate = response.output_parsed
print("Name :", candidate.name)
print("Email :", candidate.email)
print("Skills:", candidate.skills)Solution: Iteration 2
class Job(BaseModel):
company: str
title: str
start_year: int
end_year: int
class Candidate(BaseModel):
name: str
email: str
skills: list[str]
jobs: list[Job]
response = client.responses.parse(
model="gpt-6-luna",
input=[
{"role": "system", "content": "Extract the candidate information from the resume."},
{"role": "user", "content": resume},
],
text_format=Candidate,
)
candidate = response.output_parsed
for job in candidate.jobs:
print(f"{job.title} @ {job.company} ({job.start_year}-{job.end_year})")Solution: Bonus 1
class Candidate(BaseModel):
name: str
email: str
skills: list[str]
jobs: list[Job]
seniority: Literal["junior", "mid", "senior"] = Field(
description="junior: 0-2 years of experience, mid: 3-5 years, senior: more than 5 years"
)
response = client.responses.parse(
model="gpt-6-luna",
input=[
{"role": "system", "content": "Extract the candidate information from the resume."},
{"role": "user", "content": resume},
],
text_format=Candidate,
)
print(response.output_parsed.seniority)Solution: Bonus 2
class Candidate(BaseModel):
name: str
email: str | None = Field(description="Email address, or null if not in the resume")
skills: list[str]
jobs: list[Job]
seniority: Literal["junior", "mid", "senior"] = Field(
description="junior: 0-2 years of experience, mid: 3-5 years, senior: more than 5 years"
)
response = client.responses.parse(
model="gpt-6-luna",
input=[
{"role": "system", "content": "Extract the candidate information from the resume. Do not invent missing data."},
{"role": "user", "content": resume_2},
],
text_format=Candidate,
)
print(response.output_parsed.email) # NoneWith a required email: str, the schema forces the model to return something, so it may invent a value. Making the field optional lets it answer None honestly.