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.
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:
title: strcompany: strlocation: strsalary_range: str | None— optional, defaultNonerequirements: list[str]remote: bool
Step 2 — Implement the Extraction Function
Implement extract_prompt_based(text: str) -> JobPosting that:
- Calls the Claude API asking for JSON only (no preamble, no markdown fences)
- Strips markdown fences defensively in case they appear anyway
- Parses with
json.loads() - Validates with
JobPosting.model_validate(data) - Returns the validated
JobPostinginstance
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:
- JD1 has
remote=True - JD2 has
remote=False - JD3 has
remote=False(hybrid is not fully remote)
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:
name:"extract_job_posting"description: a brief descriptioninput_schema: a JSON Schema object matching theJobPostingmodel
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:
- Calls the API with
tools=[EXTRACT_TOOL]andtool_choice={"type": "tool", "name": "extract_job_posting"} - Finds the
ToolUseBlockinresponse.content - Validates
block.inputwithJobPosting.model_validate() - 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:
- The extracted data (are there any discrepancies?)
- Robustness (did either function raise an exception?)
- Token counts (look at
response.usage)
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:
- Start with a
messageslist containing the initial user prompt - In a loop up to
max_retries:- Call the API
- Try to parse and validate the response
- On success: return the
JobPosting - On
json.JSONDecodeErrororValidationError: append the assistant's response and a new user message containing the error details tomessages, then continue the loop
- 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
- Ex 1:
JobPostingmodel defined;extract_prompt_basedruns on all 3 JDs without error - Ex 2:
EXTRACT_TOOLdefined;extract_tool_basedruns on all 3 JDs; comparison table present - Ex 3:
extract_with_retryimplemented; tested on ambiguous input; retry behaviour visible in output - All cells run without error after kernel restart + run all