Lesson 02 Claude API Setup
Theme:

Lab Guide — Claude API Setup & First Calls

Three hands-on exercises to go from zero to a working multi-turn chatbot.

📥 Lab Notebooks

⬇ lab.pdf ⬇ lab_answer.pdf

📥 Demo Files

⬇ first_call.py

Prerequisites & Setup

1

Install dependencies

pip install anthropic python-dotenv
2

Create your .env file in the same folder as your notebook:

ANTHROPIC_API_KEY=sk-ant-api03-your-key-here

Get your key from console.anthropic.com → API Keys.

3

Open lab.pdf in Jupyter and run the setup cell to verify your environment.

Security reminder: Never paste your API key into notebook code cells. Always load it from .env using load_dotenv(). Add .env to your .gitignore before pushing anything to a repository.
Exercise 1 Your First API Call ~10 minutes

In this exercise you will load your API key, create an Anthropic client, make a basic messages.create() call, and print both the response text and the token usage statistics.

1

Load environment and create the client

from dotenv import load_dotenv
import os
import anthropic

load_dotenv()
client = anthropic.Anthropic()
# The SDK automatically reads ANTHROPIC_API_KEY from the environment
2

Make the API call

response = client.messages.create(
    model="claude-3-5-haiku-20241022",
    max_tokens=256,
    messages=[
        {"role": "user", "content": "Tell me a fun fact about large language models."}
    ]
)
3

Print the response text and usage

print("Response:")
print(response.content[0].text)
print()
print("Token usage:")
print(f"  Input tokens:  {response.usage.input_tokens}")
print(f"  Output tokens: {response.usage.output_tokens}")
print(f"  Stop reason:   {response.stop_reason}")

Expected output:

Response: Here's a fun fact: Large language models like me don't actually "understand" text the way humans do — we predict the most likely next token based on patterns learned from billions of examples. It's statistical pattern matching at an enormous scale! Token usage: Input tokens: 17 Output tokens: 52 Stop reason: end_turn
Tip: The exact response text will vary each time — LLMs are probabilistic. The token counts will also vary with the response length. What should be consistent is the stop_reason: end_turn, which means Claude finished naturally rather than hitting the max_tokens cap.
Exercise 2 Streaming Responses ~15 minutes

Implement stream_response(prompt: str) -> None that streams Claude's reply to the terminal, printing each chunk as it arrives. Then test it with a long prompt to see the difference in perceived latency compared to a non-streaming call.

1

Implement the function

def stream_response(prompt: str) -> None:
    """Stream a response from Claude, printing each chunk as it arrives."""
    with client.messages.stream(
        model="claude-3-5-haiku-20241022",
        max_tokens=512,
        messages=[{"role": "user", "content": prompt}]
    ) as stream:
        for text in stream.text_stream:
            print(text, end="", flush=True)
    print()  # newline after stream ends
2

Test it with a longer prompt

stream_response("Explain quantum entanglement in simple terms.")
3

Compare streaming vs non-streaming latency

import time

prompt = "Write a haiku about APIs."

# Non-streaming: waits for full response
start = time.time()
response = client.messages.create(
    model="claude-3-5-haiku-20241022",
    max_tokens=64,
    messages=[{"role": "user", "content": prompt}]
)
non_stream_time = time.time() - start
print(f"Non-streaming: got full response in {non_stream_time:.2f}s")
print(response.content[0].text)
print()

# Streaming: first token arrives quickly
print("Streaming (notice first token arrives faster):")
start = time.time()
stream_response(prompt)
stream_total_time = time.time() - start
print(f"Streaming: total time {stream_total_time:.2f}s")
Tip: The total wall-clock time for streaming is roughly the same as non-streaming. The difference is when you start seeing output. With streaming, text appears almost immediately. With a non-streaming call, nothing appears until generation is 100% complete. For a user sitting at a terminal or chatbot UI, this difference is huge.
Exercise 3 Multi-turn Conversation ~20 minutes

Build chat_loop() — an interactive chatbot that maintains the full conversation history. Claude will remember everything said in the session because you send the complete history with each API call.

1

Implement the chat loop

def chat_loop() -> None:
    """Run an interactive multi-turn conversation with Claude."""
    history = []
    print("Chat with Claude (type 'quit' to exit)\n")

    while True:
        user_input = input("You: ").strip()
        if user_input.lower() == "quit":
            print("Goodbye!")
            break

        # Append user message to history
        history.append({"role": "user", "content": user_input})

        # Call the API with the full history
        response = client.messages.create(
            model="claude-3-5-haiku-20241022",
            max_tokens=512,
            messages=history
        )

        # Extract the reply and append it to history
        assistant_reply = response.content[0].text
        history.append({"role": "assistant", "content": assistant_reply})

        # Show the reply
        print(f"Claude: {assistant_reply}\n")

chat_loop()
2

Test that Claude remembers context

Try this conversation to verify memory works:

You: My name is Alex and I love Python.
Claude: Nice to meet you, Alex! Python is a fantastic language...

You: What is my name?
Claude: Your name is Alex, as you mentioned earlier!

You: What programming language do I love?
Claude: You mentioned that you love Python!
3

Optional: add a system prompt to give Claude a persona

response = client.messages.create(
    model="claude-3-5-haiku-20241022",
    max_tokens=512,
    system="You are a friendly Python tutor. Keep answers concise and practical.",
    messages=history
)
How memory works: Claude has no built-in memory between calls. What looks like "memory" is simply the history list you maintain and pass on every API call. The API is stateless — your code is responsible for managing the conversation state. This is why very long conversations become expensive: you send more tokens with each turn.

Self-check Checklist

Before moving on, verify that you can answer "yes" to each item:

Stuck? Check lab_answer.pdf for the complete working solutions with inline comments explaining every step. Try to work through it yourself first — struggling with the code is how the concepts stick.