Lesson 06 Structured Outputs
Theme:

Lab Guide

Three exercises progressing from prompt-based JSON to tool use to a robust retry loop — all using job description extraction as the domain.

📥 Lab Notebooks

⬇ lab.pdf ⬇ lab_answer.pdf

Setup

Open lab.pdf. The first cell handles imports and creates the Anthropic client. The three job descriptions below are provided as constants in the notebook — you will use them for all exercises.

Job Descriptions (copy into your notebook if needed)

JD1 — Senior Python Developer

Senior Python Developer at TechCorp, London. Remote OK. £80-100K.
Must have: 5+ yrs Python, FastAPI, PostgreSQL.
Nice to have: Kubernetes, Rust.

JD2 — Marketing Manager

Marketing Manager - GrowthCo, NYC. On-site. $75,000-$90,000/yr.
Requirements: 3+ years B2B SaaS marketing, HubSpot experience, strong writing skills.
Lead a team of 2.

JD3 — Junior ML Engineer

Junior ML Engineer, Berlin, Germany. Hybrid (3 days office). €45K-€60K.
Requirements: Python, PyTorch or TensorFlow, math fundamentals.
Recent graduates welcome.

Exercise 1 — Prompt-Based JSON Extraction

Goal

Extract structured job posting data using only prompt instructions (no tool use). Validate the output with Pydantic.

Step 1 — Define the Pydantic Model

Define JobPosting with these fields:

Step 2 — Implement the Extraction Function

Implement extract_prompt_based(text: str) -> JobPosting that:

  1. Calls the Claude API asking for JSON only (no preamble, no markdown fences)
  2. Strips markdown fences defensively in case they appear anyway
  3. Parses with json.loads()
  4. Validates with JobPosting.model_validate(data)
  5. Returns the validated JobPosting instance

Step 3 — Test on All 3 JDs

Call the function on JD1, JD2, and JD3. Print the result of posting.model_dump() for each. Verify that:

Defensive stripping tip: the following snippet handles the most common markdown-fence variants:

raw = raw.strip()
if raw.startswith("```"):
    # Remove opening fence (```json or ``` etc.)
    raw = raw.split("\n", 1)[1] if "\n" in raw else raw[3:]
    # Remove closing fence
    if raw.endswith("```"):
        raw = raw[:-3].strip()

Exercise 2 — Tool Use Extraction

Goal

Re-implement the extraction using the Claude tool use API, which guarantees the output matches the schema.

Step 1 — Define the Tool

Define a dict EXTRACT_TOOL with:

Shortcut: you can generate the JSON Schema automatically from your Pydantic model: JobPosting.model_json_schema(). You'll need to remove the title key from the top level and pass the rest as input_schema.

Step 2 — Implement extract_tool_based

Implement extract_tool_based(text: str) -> JobPosting that:

  1. Calls the API with tools=[EXTRACT_TOOL] and tool_choice={"type": "tool", "name": "extract_job_posting"}
  2. Finds the ToolUseBlock in response.content
  3. Validates block.input with JobPosting.model_validate()
  4. Returns the validated JobPosting

Step 3 — Compare with Exercise 1

Run both extract_prompt_based and extract_tool_based on all 3 JDs. In a Markdown cell, note any differences in:

Exercise 3 — Retry on Validation Error

Goal

Implement a retry loop that feeds ValidationError details back to Claude to recover from extraction failures.

Instructions

Implement extract_with_retry(text: str, max_retries: int = 3) -> JobPosting:

  1. Start with a messages list containing the initial user prompt
  2. In a loop up to max_retries:
    • Call the API
    • Try to parse and validate the response
    • On success: return the JobPosting
    • On json.JSONDecodeError or ValidationError: append the assistant's response and a new user message containing the error details to messages, then continue the loop
  3. If all retries are exhausted, raise the last exception

Test Cases

Test on the following deliberately ambiguous input — it should trigger at least one retry or return a best-effort extraction:

We're looking for someone awesome to join our growing team.
Good pay. Cool office. Tech stuff. Apply now!

Also test on the three normal JDs to confirm it still works on well-formed input.

Timeout guard: add import time and log the attempt number and timestamp so you can see the retry loop in action without losing track of costs.

Completion Checklist