learn-claude-with-phoebe / Deep dive 6.1
Learn Claude with Phoebe · Deep-dive track 6.1

The Claude API

Everything before this page happened inside an app someone else built. Today you call Claude from your own Python: one function, a messages list, and a response object. By the end you can hold a conversation, stream tokens, and get validated JSON back - the foundation every later deep dive builds on.

🔴 Deep dive DS & AI 45 min self-paced or live
0-5 · Setup 5-35 · Core 35-45 · Try it
Part 0

Why this page exists

The chat window is a product wrapped around an API. Once you call the API directly, Claude becomes a component: something you can put inside a pipeline, a notebook, a cron job, or a data quality check. This page covers the "Accessing Claude with the API" module that all three official engineering courses share, plus the API fundamentals from Claude Platform 101.

Core - do these in order Advanced - read once, return when needed Shared module of all 3 engineering courses
★ What you walk out with today A working API setup, a multi-turn chat loop you wrote yourself, a streaming call, and a JSON extractor with validation and retries - the four moves that cover 90% of real API work.
Part 1 · first contact

One call, fully understood 12 min

The whole API is one method: client.messages.create(). Learn its parameters properly once and every advanced feature is just another argument.

CoreSetup and your first request5 min

Three steps: install the SDK, set your key as an environment variable (never hardcode it - keys in notebooks end up in git), make the call.

★ Terminal - one-time setuppip install anthropic export ANTHROPIC_API_KEY="sk-ant-..." # get one at console.anthropic.com
★ Python - your first requestimport anthropic client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY from the environment response = client.messages.create( model="claude-sonnet-5", max_tokens=1024, messages=[ {"role": "user", "content": "Explain what a data catalog is in 3 sentences."} ], ) print(response.content[0].text)

Read the response object once, carefully: response.content is a LIST of blocks (later, tool calls appear here too), so the text lives at response.content[0].text. response.usage tells you input and output token counts - that is your bill. response.stop_reason tells you why generation ended; "max_tokens" there means you cut Claude off mid-answer.

CoreAnatomy of a request: model, max_tokens, temperature7 min

Three parameters do most of the work. Get the mental model right for each:

Which model tier for this call Does the task need hard reasoning? yes no Capable tier Opus, Sonnet Fast tier: Haiku classification, routing Prototype on the capable tier first; move to Haiku only once quality already holds.
🔍 Click to zoom - prototype on the capable tier, downgrade to Haiku once quality holds
ParameterWhat it controlsHow to set it
modelCapability vs speed vs costCapable tier (Opus, Sonnet) for reasoning and hard extraction; fast tier (Haiku) for classification, routing, high-volume simple tasks
max_tokensHard ceiling on OUTPUT lengthA budget, not a target - Claude stops naturally before it. Set generously (1024-4096); a truncated answer is worse than a few unused tokens
temperatureSampling randomness, 0 to 10 for extraction, classification, anything you will parse (near-deterministic); around 1 for brainstorming and creative variety. Default is 1
★ Extraction settings vs creative settings# Parsing the output? Pin it down: extraction = client.messages.create( model="claude-sonnet-5", max_tokens=1024, temperature=0, messages=[{"role": "user", "content": "List the column names in this DDL: ..."}], ) # Want variety? Open it up: brainstorm = client.messages.create( model="claude-sonnet-5", max_tokens=2048, temperature=1, messages=[{"role": "user", "content": "Give me 10 names for a data quality dashboard."}], )
The model rule: prototype on best, then optimize Always build with the most capable model first. If the task fails there, it is a prompt problem. Once it works, try the fast tier - if quality holds, you just cut cost and latency. Doing it in the other order means debugging a prompt and a model at the same time.
Part 2 · conversations

Multi-turn and system prompts 10 min

The API has no memory. Every call is a blank slate - "conversation" is something YOU maintain by resending history. This surprises everyone once.

CoreMulti-turn: the messages list IS the conversation6 min

The messages parameter is not "the new message" - it is the WHOLE conversation so far, alternating user and assistant roles. To continue a chat, you append Claude's reply to your list and send everything again next call. The server stores nothing between calls.

★ A complete chat loop with historyimport anthropic client = anthropic.Anthropic() history = [] while True: user_input = input("You: ") if user_input.lower() in ("quit", "exit"): break history.append({"role": "user", "content": user_input}) response = client.messages.create( model="claude-sonnet-5", max_tokens=1024, messages=history, # the ENTIRE conversation, every call ) reply = response.content[0].text history.append({"role": "assistant", "content": reply}) # remember the answer too print("Claude:", reply)

Two rules the API enforces: roles must alternate (user, assistant, user, ...), and the last message is normally from user. Forget to append the assistant turn and Claude "forgets" its own previous answer - the #1 beginner bug.

Real world

A team built a support-ticket summarizer that "kept losing context" after the second question. The bug: they sent only the newest user message each call. One line - appending the assistant reply to history - fixed it. Cost implication too: since you resend history, long conversations grow in input tokens every turn. Production apps truncate or summarize old turns; 6.5 covers prompt caching, which makes resent history nearly free.

CoreSystem prompts: the standing instructions4 min

The system parameter is a separate top-level argument, not a message. It sets who Claude is for the whole conversation: role, rules, output format. The messages list then carries the actual work items.

★ System vs user - the division of laborresponse = client.messages.create( model="claude-sonnet-5", max_tokens=1024, system=( "You are a senior data engineer reviewing SQL for a retail warehouse. " "Always answer with: 1) verdict (OK / needs work), 2) issues as a numbered " "list, 3) the corrected query. Flag full-table scans and missing WHERE " "clauses on partitioned tables. Be terse." ), messages=[ {"role": "user", "content": "SELECT * FROM orders JOIN customers ON true"} ], ) print(response.content[0].text)

Rule of thumb: if it should be true for EVERY turn (persona, rules, format, tone), it belongs in system. If it is this particular task's data or question, it belongs in a user message. Mixing them - stuffing documents into system, or repeating rules in every user turn - works, but degrades both steering and cost.

Where does this content belong: system or user ✗ Mixed system and user Rules repeated every user turn Documents stuffed into system Degrades steering and cost ✓ Clean separation Persona and rules in system once Task data lives in messages Better steering, lower cost If it's true every turn it belongs in system; this task's data belongs in a user message.
🔍 Click to zoom - standing rules live in system, task data lives in messages
Part 3 · production patterns

Streaming, JSON, and failure 13 min

Three patterns separate a demo from a tool people use: tokens that appear as they generate, output your code can parse, and calls that survive a bad network day.

CoreStreaming: tokens as they arrive4 min

A long answer can take 20+ seconds to finish. Without streaming your user stares at nothing; with it, text flows immediately - same total time, completely different experience. The SDK makes it a context manager:

★ Streaming to the consoleimport anthropic client = anthropic.Anthropic() with client.messages.stream( model="claude-sonnet-5", max_tokens=2048, messages=[ {"role": "user", "content": "Explain the medallion architecture, layer by layer."} ], ) as stream: for text in stream.text_stream: print(text, end="", flush=True) print() # final newline final = stream.get_final_message() # the assembled Message object, if you need usage etc.

Use streaming for anything a human watches (chat UIs, CLI tools, Streamlit apps). Skip it for batch pipelines - there, nobody is watching and the plain call is simpler code.

CoreStructured output: the JSON prefill trick5 min

Ask for JSON and Claude often adds a friendly preamble ("Here is the JSON you requested:") that breaks json.loads. The fix is prefilling: you supply the START of the assistant's answer as the last message, and Claude continues from it. Start it with { and there is nowhere for a preamble to go.

★ Extractor with prefill, validation, and retryimport json import anthropic client = anthropic.Anthropic() def extract(email_text: str, retries: int = 2) -> dict: for attempt in range(retries + 1): response = client.messages.create( model="claude-sonnet-5", max_tokens=1024, temperature=0, system=( 'Extract fields from the email. Respond with ONLY this JSON: ' '{"sender_name": str, "company": str, "request": str, "urgency": "low"|"medium"|"high"}' ), messages=[ {"role": "user", "content": email_text}, {"role": "assistant", "content": "{"}, # the prefill ], ) raw = "{" + response.content[0].text # glue the prefill back on try: return json.loads(raw) # validate before trusting except json.JSONDecodeError: continue # rare at temperature 0; retry raise ValueError("No valid JSON after retries") print(extract("hi its mia from acme, our friday dashboard is broken AGAIN, need it before the board mtg!!"))

The pattern in one line: describe the exact schema in system, prefill {, glue it back on, json.loads before you trust anything. In 6.3 you will see tool use, which gives schema-enforced JSON - but this trick needs zero extra machinery and covers most extraction jobs.

AdvancedError handling and rate limits4 min

Two failures you WILL meet: 429 rate_limit_error when you send requests faster than your tier allows, and occasional 5xx or timeouts like any remote service. Both are retryable; the standard answer is exponential backoff.

★ Backoff wrapperimport time import anthropic client = anthropic.Anthropic(timeout=60.0) # do not wait forever on a hung connection def call_with_retry(messages, max_retries=5): for attempt in range(max_retries): try: return client.messages.create( model="claude-sonnet-5", max_tokens=1024, messages=messages, ) except anthropic.RateLimitError: wait = 2 ** attempt # 1, 2, 4, 8, 16 seconds print(f"429 - backing off {wait}s") time.sleep(wait) except anthropic.APIStatusError as err: if err.status_code >= 500: # server side: worth retrying time.sleep(2 ** attempt) else: # 400s: your request is wrong, retrying won't help raise raise RuntimeError("Retries exhausted")

Notes for real pipelines: the SDK already retries some errors by itself (tune with max_retries= on the client); rate limits are per model and measured in requests AND tokens per minute, so batch jobs should throttle on tokens; and a 400 means fix your request, not retry it. For huge offline jobs, the Batch API (half price, results within 24h) is usually the right tool - it comes up in 6.3.

Try it yourself

Three builds ◐ 10 min in session, finish after

Stuck? Paste your exact code and the full traceback into Claude and ask it to diagnose. Debugging API calls with the API is not cheating - it is the workflow.
Source material

Official courses covered

This page teaches the shared "Accessing Claude with the API" module from the three official 8-hour engineering courses at claude.com/resources/courses, plus the API fundamentals from Claude Platform 101.

Building with the Claude API - Accessing Claude modulerequests, multi-turn, system prompts, temperature, streaming, structured data
Claude with Bedrock / Vertex - same moduleidentical content; platform setup differences covered in 6.8
Claude Platform 101 - API fundamentalsconsole, keys, model tiers, request anatomy

Deep dive 6.1 cheat sheet · pin this

The one callclient.messages.create(model, max_tokens, messages) → response.content[0].text. Everything else is arguments.
Model rulePrototype on the most capable model, then step down to the fast tier and check quality holds.
Multi-turnNo server memory. messages = the whole history, resent every call - append the assistant reply too.
System vs userTrue every turn (role, rules, format) → system. This task's data and question → user message.
Streamingwith client.messages.stream(...) as stream: for text in stream.text_stream. Use whenever a human watches.
JSON prefillPrefill assistant with "{", glue it back on, json.loads before trusting, retry on parse failure.