Skip to content

Instantly share code, notes, and snippets.

@luisjunco
Created October 14, 2026 07:54
Show Gist options
  • Select an option

  • Save luisjunco/d6e4b11eeb5777557ecc859881a96296 to your computer and use it in GitHub Desktop.

Select an option

Save luisjunco/d6e4b11eeb5777557ecc859881a96296 to your computer and use it in GitHub Desktop.
Practice: Structured Outputs

Practice: Resume Parser with Structured Outputs

Instructions

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
"""

Iteration 1: Basic fields

  1. Create a Pydantic model called Candidate with these fields:
    • name (string)
    • email (string)
    • skills (list of strings)
  2. Call client.responses.parse() with the resume as input and Candidate as the text_format.
  3. Print name, email, and skills from response.output_parsed.

Iteration 2: Nested data

  1. Create a second model called Job with: company, title, start_year (int), end_year (int).
  2. Add a field jobs to Candidate. It should be a list of Job.
  3. Run the call again and print each job in a loop, for example Data Analyst @ Acme Corp (2021-2024).

Bonus iterations

Bonus 1: Restricted values

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).

Bonus 2: Missing information

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.

Solutions

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)  # None

With 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment