Working with the Claude API

Chapter 9 — Scenario: Working with the Claude API

You already have experience working with Claude through Claude Code. The Claude API lets you go further — calling Claude programmatically from your own Python scripts, building it into applications, automating tasks, and experimenting with prompts in a controlled environment.

In this chapter you'll build a dedicated Docker container for Claude API development: a clean Python environment with the Anthropic SDK pre-installed, your API key handled safely through environment variables (never baked into an image), and a script structure you can extend for any Claude-powered project.

What you need: An Anthropic API key from console.anthropic.com. If you don't have one yet, you can follow this chapter and substitute a placeholder key — the container setup and code structure are the same. API keys start with sk-ant-api03-...
1
Keeping Your API Key Safe

An API key is a password. Leak it and anyone who finds it can use your Anthropic account, run up a bill, and access anything you've built. The Docker workflow makes it easy to handle keys correctly — but also easy to get wrong if you're not paying attention.

.env file on your local machine — key lives here, never leaves this machine SAFE
docker run --env-file .env — injects key as environment variable at runtime SAFE
os.environ["ANTHROPIC_API_KEY"] — read in Python at runtime, never stored SAFE
ENV ANTHROPIC_API_KEY=sk-ant-... in Dockerfile — baked into every layer, visible in image history DANGER
api_key = "sk-ant-..." hardcoded in .py file — one accidental git push and it's public DANGER
.env file committed to git — git history preserves it even after deletion DANGER
If you ever accidentally expose a key: Immediately go to console.anthropic.com → API Keys and revoke it. Create a new key. Revoking takes effect immediately — the leaked key stops working the moment you revoke it.
2
Project Structure
claude-api-dev/ ├── Dockerfile ├── requirements.txt ├── .env # ANTHROPIC_API_KEY=sk-ant-... — NEVER commit this ├── .env.example # ANTHROPIC_API_KEY=your-key-here — safe to commit ├── .dockerignore ├── .gitignore └── scripts/ ├── hello_claude.py # Basic API call — start here ├── conversation.py # Multi-turn conversation loop ├── streaming.py # Streaming responses └── batch_process.py # Process a list of inputs
# .gitignore (and .dockerignore — add .env to both!) .env .env.* !.env.example # allow the example file through __pycache__ *.pyc .venv venv
# .env (your actual key — stays on your machine) ANTHROPIC_API_KEY=sk-ant-api03-your-real-key-here # .env.example (placeholder — safe to commit to git) ANTHROPIC_API_KEY=your-anthropic-api-key-here
3
Dockerfile and Requirements
# Dockerfile FROM python:3.12-slim WORKDIR /workspace # Install the Anthropic SDK and supporting packages COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # Source code will be bind-mounted — no COPY here # API key will be injected at runtime — no ENV here CMD ["python", "-c", "print('Claude API dev container ready.')"]
# requirements.txt anthropic==0.28.0 # Anthropic SDK (pin the version) python-dotenv==1.0.1 # load .env files in scripts rich==13.7.1 # pretty terminal output httpx==0.27.0 # async HTTP (already a dep of anthropic)
Terminal — build the image
$ docker build -t claude-dev:latest . [+] Building 9.4s (6/6) FINISHED => [1/3] FROM python:3.12-slim 2.8s => [2/3] COPY requirements.txt . 0.0s => [3/3] RUN pip install --no-cache-dir -r requirements.txt 6.1s # Confirm the SDK is installed $ docker run --rm claude-dev python -c "import anthropic; print(anthropic.__version__)" 0.28.0
4
Your First API Call
# scripts/hello_claude.py import os import anthropic from dotenv import load_dotenv # load_dotenv() reads .env from the current working directory # If the key is already in the environment (injected by Docker), this is a no-op load_dotenv() client = anthropic.Anthropic( api_key=os.environ["ANTHROPIC_API_KEY"] ) message = client.messages.create( model="claude-sonnet-4-6", max_tokens=1024, messages=[ { "role": "user", "content": "Explain what Docker is in exactly two sentences." } ] ) print(message.content[0].text)
Terminal — run with API key injected safely
# --env-file injects the key at runtime — it is NEVER stored in the image $ docker run --rm ` --env-file .env ` -v "${PWD}/scripts:/workspace/scripts" ` claude-dev ` python scripts/hello_claude.py Docker is a platform that packages applications and their dependencies into lightweight, portable containers that run consistently across any environment. Unlike virtual machines, containers share the host operating system kernel, making them faster to start and more efficient with resources.

Understanding the API response object

{ "id": "msg_01XFDUDYJgAACzvnptvVoYEL", "type": "message", "role": "assistant", "model": "claude-sonnet-4-6", "content": [ { "type": "text", "text": "Docker is a platform that..." ← message.content[0].text } ], "stop_reason": "end_turn", "usage": { "input_tokens": 18, ← what you sent "output_tokens": 67 ← what Claude generated (you pay for both) } }
Always log token usage during development. Add print(f"Tokens: {message.usage.input_tokens} in / {message.usage.output_tokens} out") to your scripts while experimenting. It's easy to accidentally write a loop that calls the API hundreds of times and run up a large bill — watching the token count keeps you aware of what you're spending.
5
Choosing a Model
Model IDBest forSpeedCost
claude-haiku-4-5-20251001 High-volume tasks, classification, summarisation, simple Q&A Fastest Lowest
claude-sonnet-4-6 General development, code generation, analysis, most everyday tasks Fast Mid
claude-opus-4-8 Complex reasoning, nuanced writing, hard problems requiring deep thought Slower Higher
Development tip: Use claude-haiku-4-5-20251001 while building and testing your scripts — it's fast and cheap, so iteration is painless. Switch to claude-sonnet-4-6 or claude-opus-4-8 only when you need more capability. A single model string at the top of your script makes switching trivial.
6
Useful Patterns
System prompt
Sets Claude's persona, instructions, or constraints for the entire conversation. Passed separately from the user message.
Use for: giving Claude a role, restricting output format, injecting context
Multi-turn conversation
Pass the full message history on every call — the API is stateless. Your code accumulates the messages list.
Use for: chatbots, interactive tools, anything needing context from earlier turns
Streaming
Receive tokens as they're generated instead of waiting for the full response. Dramatically better UX for long outputs.
Use for: any user-facing interface, long responses, progress feedback
Batch processing
Loop through a list of inputs, call the API for each, collect results. Add a delay or rate-limit check between calls.
Use for: processing files, summarising a list of articles, classifying records

System prompt

# Add a system parameter to messages.create() message = client.messages.create( model="claude-sonnet-4-6", max_tokens=1024, system="""You are a concise technical writer. Always respond in plain text, no markdown. Keep answers under 100 words unless asked for more.""", messages=[ {"role": "user", "content": "What is a Docker volume?"} ] )

Multi-turn conversation

# scripts/conversation.py import os, anthropic from dotenv import load_dotenv load_dotenv() client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"]) history = [] # accumulate the full conversation print("Chat with Claude (type 'quit' to exit)\n") while True: user_input = input("You: ").strip() if user_input.lower() in ("quit", "exit", ""): break history.append({"role": "user", "content": user_input}) response = client.messages.create( model="claude-sonnet-4-6", max_tokens=1024, messages=history # send the full history every time ) reply = response.content[0].text history.append({"role": "assistant", "content": reply}) print(f"\nClaude: {reply}\n")
Terminal — run the conversation loop interactively
# -it is essential for interactive input (stdin attached) $ docker run -it --rm ` --env-file .env ` -v "${PWD}/scripts:/workspace/scripts" ` claude-dev ` python scripts/conversation.py Chat with Claude (type 'quit' to exit) You: What is the capital of Hungary? Claude: Budapest is the capital of Hungary. You: And what language do they speak there? Claude: They speak Hungarian (Magyar) — Claude remembers context from the previous turn. You: quit

Streaming responses

# scripts/streaming.py import os, anthropic from dotenv import load_dotenv load_dotenv() client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"]) print("Claude: ", end="", flush=True) # stream=True returns a context manager — tokens arrive as they're generated with client.messages.stream( model="claude-sonnet-4-6", max_tokens=512, messages=[{"role": "user", "content": "Write a short poem about containers."}] ) as stream: for text in stream.text_stream(): print(text, end="", flush=True) print() # newline after streaming finishes

Batch processing a list of inputs

# scripts/batch_process.py import os, time, anthropic from dotenv import load_dotenv load_dotenv() client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"]) # Example: classify a list of sentences as positive/negative sentences = [ "I love learning new things every day.", "This is incredibly frustrating.", "Docker makes deployment so much simpler.", "I can't get this to work no matter what I try.", ] for i, sentence in enumerate(sentences, 1): response = client.messages.create( model="claude-haiku-4-5-20251001", # use Haiku for high-volume batch work max_tokens=10, system="Classify the sentiment as exactly one word: positive, negative, or neutral.", messages=[{"role": "user", "content": sentence}] ) sentiment = response.content[0].text.strip() print(f"[{i}/{len(sentences)}] {sentiment:10s} — {sentence}") time.sleep(0.5) # gentle rate limiting between calls
Terminal — run the batch script
$ docker run --rm ` --env-file .env ` -v "${PWD}/scripts:/workspace/scripts" ` claude-dev ` python scripts/batch_process.py [1/4] positive — I love learning new things every day. [2/4] negative — This is incredibly frustrating. [3/4] positive — Docker makes deployment so much simpler. [4/4] negative — I can't get this to work no matter what I try.
7
Persistent Shell for Experimentation

When you're actively experimenting — trying different prompts, inspecting response objects, testing ideas — it's more efficient to keep a container running and exec into it rather than starting a fresh container for every script run.

Terminal — long-lived dev container
# Start a long-lived container (no --rm, no specific command) $ docker run -d ` --name claude-dev ` --env-file .env ` -v "${PWD}/scripts:/workspace/scripts" ` claude-dev ` sleep infinity 3a9b1c... # Run any script without starting a new container each time $ docker exec claude-dev python scripts/hello_claude.py $ docker exec claude-dev python scripts/streaming.py # Open an interactive Python REPL inside the container $ docker exec -it claude-dev python >>> import anthropic, os >>> client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"]) >>> r = client.messages.create(model="claude-haiku-4-5-20251001", max_tokens=50, messages=[{"role":"user","content":"hi"}]) >>> r.content[0].text 'Hello! How can I help you today?' >>> r.usage Usage(input_tokens=8, output_tokens=12) # When done experimenting, stop the container $ docker stop claude-dev && docker rm claude-dev
save-output.ps1 — capture Claude's response to a file:
docker exec claude-dev python scripts/hello_claude.py | Out-File -Encoding utf8 output.txt
Useful when generating long content you want to review or use elsewhere.
8
Error Handling
import os, anthropic from dotenv import load_dotenv load_dotenv() # Fail early with a clear message if the key isn't set api_key = os.environ.get("ANTHROPIC_API_KEY") if not api_key: raise EnvironmentError("ANTHROPIC_API_KEY not set. Add it to .env or pass with --env-file.") client = anthropic.Anthropic(api_key=api_key) try: message = client.messages.create( model="claude-sonnet-4-6", max_tokens=1024, messages=[{"role": "user", "content": "Hello!"}] ) print(message.content[0].text) except anthropic.AuthenticationError: print("Invalid API key — check your .env file") except anthropic.RateLimitError: print("Rate limit hit — slow down or upgrade your plan") except anthropic.APIConnectionError: print("No connection to Anthropic API — check your network") except anthropic.APIStatusError as e: print(f"API error {e.status_code}: {e.message}")
Exercises
  1. Run hello_claude.py. Create all the files from Steps 2–4 (Dockerfile, requirements.txt, .env with your real key, .dockerignore, .gitignore, and scripts/hello_claude.py). Build the image and run the script with --env-file .env. Confirm Claude's response appears in the terminal. Then run docker run --rm claude-dev env | grep ANTHROPIC to see the key is injected — and run docker image history claude-dev:latest to confirm it is NOT baked into any image layer.
  2. Experiment with models. Edit hello_claude.py to use claude-haiku-4-5-20251001 instead of Sonnet. Add a line to print the token usage after the response. Run it with the same prompt and note the token counts. Try the same with Sonnet. Is the response quality noticeably different for a simple question? Try a harder question where Haiku struggles.
  3. Build the conversation loop. Create scripts/conversation.py from Step 6 and run it with docker run -it. Have a multi-turn conversation about something you're learning (Japanese, Docker, Python — anything). After you quit, notice that the next time you run it, Claude has no memory of the previous session — because history was stored in a Python list that was discarded when the container exited. Think about how you'd save that history to a file to persist it across runs.
  4. Streaming vs non-streaming. Create scripts/streaming.py and run it. Notice how the response appears word-by-word instead of all at once after a delay. Now change the prompt to ask for something long (e.g. "Write a detailed explanation of how Docker networking works"). The difference in perceived responsiveness between streaming and non-streaming is most obvious with longer responses.
  5. Build something useful for your own work. Write a new script that uses Claude to help with something you actually do. Examples: summarise a webpage (paste in text), generate a lesson plan outline, translate a short paragraph to Hungarian or Japanese, review a piece of Python code you're working on, or generate test cases for a function. The goal is to go from "API example" to "tool I'd actually use" — even if it's simple.
Next: Chapter 10 — Docker Compose: Running Multi-Container Apps
Throughout this course you've been starting containers one by one with docker run. Chapter 10 introduces Docker Compose — a single YAML file that defines your entire multi-container stack (web server, database, cache, tunnel) and brings it all up with one command. It's the natural endpoint of the course, and the gateway to everything beyond: production deployments, CI pipelines, and the intermediate Docker course.