Build a Multi-Turn AI Chatbot

Call the Claude API to build a streaming chatbot with conversation memory.

Introduction

30 Second Summary

Every AI product you use daily, from coding assistants to customer support bots, is built on the same core pattern: sending messages to a large language model and managing the conversation. Understanding this pattern is the single most valuable skill in AI engineering today.

In this project, you will build a terminal-based chatbot powered by Claude that acts as your personal coding tutor. You will start with a single API call and end with a polished interactive experience that streams responses in real time.

What You'll Build

A Python chatbot that calls the Claude Messages API, remembers your conversation history, and streams responses word-by-word in your terminal.

By the end of this project, you'll have:

  • A multi-turn chatbot that maintains full conversation context across messages.
  • Real-time streaming responses so output appears word-by-word instead of all at once.
  • A system prompt and error handling that make your chatbot behave like a focused coding tutor.
  • Secret Mission: Add token counting and cost tracking so you can see exactly how much each conversation costs in real time.

Are there any prerequisites?

None. This guide walks you through installing every tool you need from scratch, including Python and VS Code. All you need is a computer and an internet connection.

Set Up Your Development Environment

Before you write any code, you need a place to write it and the right tools to run it.

In this step, you'll install Python 3, set up VS Code as your code editor, and create an isolated environment for your chatbot project.

In this step, get ready to:
  • Install and verify Python 3.
  • Set up VS Code with a new project folder.
  • Create and activate a Python virtual environment.
Check that Python 3 is installed

Python is the programming language you'll use to build your chatbot. You need Python 3 installed on your machine to run the code.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
  • Check your Python version by running this command:
python3 --version

✔️ I see a version number

You should see something like Python 3.12.x or Python 3.11.x. Any version of Python 3 works for this project. You're ready to move on.

ⓧ Command not found

Python 3 is not installed on your machine yet. You need to download and install it.

  • Go to https://www.python.org/downloads in your browser.
  • Click the large Download Python button. This gives you the latest Python 3 version for your operating system.
  • Run the installer. On Windows, make sure to check the Add python.exe to PATH checkbox before clicking Install Now.
  • Once the installer finishes, close and reopen your terminal.
  • Run python3 --version again to confirm the installation.

Still not working after installing?

On Windows, try python --version (without the 3). Some Windows installers register the command as python instead of python3.

help me troubleshoot my Python installation

Create your project folder and open VS Code

VS Code is a free code editor where you'll write and run your chatbot. You'll also create a dedicated folder to keep all your project files organized.

Don't have VS Code yet?

Download it for free from https://code.visualstudio.com/download. Run the installer and follow the prompts. Once installed, come back here and continue.

  • Move to your Desktop by running this command in your terminal:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir coding-tutor-chatbot
cd coding-tutor-chatbot
  • Confirm the folder was created by running:
ls

The folder will be empty for now. That's expected.

Now open VS Code and point it at your new folder.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Visual Studio Code and press Enter to open it.
  • In VS Code, click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop and select the coding-tutor-chatbot folder, then click Open.

You should see coding-tutor-chatbot in the Explorer sidebar on the left. The folder will be empty.

Don't see the Explorer sidebar?

Press Cmd+Shift+E (macOS) or Ctrl+Shift+E (Windows) to toggle it open.

help me find the Explorer panel in VS Code

Create and activate a Python virtual environment

A virtual environment is an isolated space where you install packages for this project only. It prevents conflicts with other Python projects on your machine.

  • Open VS Code's integrated terminal by pressing Cmd+` (macOS) or Ctrl+` (Windows).

A terminal panel opens at the bottom of VS Code. It should already be pointed at your coding-tutor-chatbot folder.

  • Create the virtual environment by running this command:
python3 -m venv venv

What does this command do?

python3 -m venv tells Python to use its built-in virtual environment module. The second venv is the name of the folder where the environment will live.

  • Confirm the venv folder was created by running:
ls

You should see a venv folder in the output.

Now activate the virtual environment. The command differs by operating system.

macOS

  • Activate the virtual environment by running:
source venv/bin/activate

Windows

  • Activate the virtual environment by running:
venv\Scripts\activate

You should see (venv) appear at the beginning of your terminal prompt. This confirms the virtual environment is active.

Don't see (venv) in your prompt?

Make sure you ran the activation command for your operating system. On Windows, if you see an error about execution policies, run Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned first, then try the activation command again.

help me activate my Python virtual environment

Your development environment is ready. Next up, you'll get your Claude API key so your chatbot can talk to a real AI model.

Get Your Claude API Key

Every API call to Claude requires authentication. You prove your identity to the Anthropic servers using an API key, which is a unique string tied to your account.

In this step, you will create an Anthropic Console account, purchase API credits, and store your key as an environment variable so your Python code can find it automatically.

In this step, get ready to:
  • Create an Anthropic Console account.
  • Purchase $5 in API credits.
  • Store your API key as an environment variable.
Create an Anthropic Console account

The Anthropic Console is where you manage your API keys, billing, and usage. You need an account before you can make any API calls.

  • Open your browser and go to console.anthropic.com.
  • Click Sign Up and create your account using an email address or a Google account.
  • Complete any verification steps (email confirmation) that Anthropic requires.

Why do I need a separate Console account?

The Anthropic Console is a different product from claude.ai (the chatbot). The Console is for developers who want to build applications using the Claude API. Your Console account manages API keys, billing, and usage tracking.

Purchase API credits

The Claude API uses prepaid credits. You purchase credits upfront and each API call deducts a tiny amount based on how many tokens you send and receive.

  • In the Console, navigate to Settings in the left sidebar.
  • Click Billing.
  • Click the Buy credits button.
  • Enter $5 as the amount (this is the minimum required to access Tier 1).
  • Enter your payment details and confirm the purchase.

How much will this project cost?

Claude Haiku 4.5 costs $1.00 per million input tokens and $5.00 per million output tokens. A full learning session with this chatbot costs well under $0.05. Your $5 purchase gives you enough credits for hundreds of conversations.

Credits expire one year from the purchase date. They sit idle when you are not making API calls.

Credits are non-refundable

The $5 minimum purchase is non-refundable. However, your credits remain available for a full year and can be used across any Anthropic API project.

Generate an API key and store it as an environment variable

Your API key is the credential your Python code uses to authenticate with Claude. The Anthropic SDK automatically reads it from an environment variable called ANTHROPIC_API_KEY.

  • In the Console left sidebar, click API Keys.
  • Click Create Key.
  • Enter a name for your key (e.g. coding-tutor).
  • Click Create.
  • Copy the key immediately. It will only be shown once.

Copy your key now

The full API key is displayed only at the moment of creation. Once you close this dialog, you cannot view the full key again. If you lose it, you will need to generate a new one.

Now store the key as an environment variable in your terminal. Switch to your VS Code terminal (the one with your virtual environment active).

macOS

  • Set the environment variable by running this command (replace your-api-key-here with the key you just copied):
export ANTHROPIC_API_KEY='your-api-key-here'

What does this command do?

The export command creates an environment variable that is accessible to any program you run in this terminal session. The Anthropic Python SDK looks for ANTHROPIC_API_KEY automatically when you create a client.

Make it permanent

This variable only lasts for the current terminal session. To persist it, add the same export line to your shell profile (~/.zshrc or ~/.bashrc).

Windows

  • Set the environment variable by running this command in PowerShell (replace your-api-key-here with the key you just copied):
$env:ANTHROPIC_API_KEY = "your-api-key-here"

What does this command do?

The $env: prefix sets an environment variable in your current PowerShell session. The Anthropic Python SDK looks for ANTHROPIC_API_KEY automatically when you create a client.

Make it permanent

This variable only lasts for the current terminal session. To persist it across sessions, use the System Environment Variables dialog in Windows Settings, or run the following command:

[System.Environment]::SetEnvironmentVariable("ANTHROPIC_API_KEY", "your-api-key-here", "User")
  • Verify the variable is set by running:

macOS

echo $ANTHROPIC_API_KEY

Windows

echo $env:ANTHROPIC_API_KEY

You should see your API key printed in the terminal. If you see an empty line, re-run the export command from the previous step.

Seeing a blank output?

Make sure you ran the export command in the same terminal window where you are checking. If you opened a new terminal tab, the variable will not carry over unless you added it to your shell profile.

Also check that you did not accidentally include extra spaces around the = sign in your export command.

help me debug why my ANTHROPIC_API_KEY environment variable is empty

Your API key is ready. In the next step, you will install the Anthropic Python SDK and make your first API call to Claude.

Make Your First API Call

Your API key is set and ready to go. Now comes the exciting part: actually talking to Claude from your own code.

You'll install the Anthropic Python SDK and write a short script that sends a message to Claude Haiku 4.5. By the end of this step, you'll have a working API call that proves everything is connected.

In this step, get ready to:
  • Install the Anthropic Python SDK.
  • Write a script that sends a single message to Claude and prints the response.
  • Run the script and verify you get a response from Claude Haiku 4.5.
Install the Anthropic Python SDK

The Anthropic Python SDK is a library that handles the HTTP connection, authentication, and response parsing when you talk to Claude. It reads your ANTHROPIC_API_KEY environment variable automatically so you never need to hardcode credentials.

  • Install the SDK by running this command in your terminal (make sure your virtual environment is still active):
pip install anthropic
  • You should see output ending with Successfully installed anthropic-... (the exact version number may vary).

Seeing 'command not found' or permission errors?

Make sure your virtual environment is active. You should see (venv) at the start of your terminal prompt. If not, activate it again by running source venv/bin/activate (macOS/Linux) or venv\Scripts\activate (Windows).

help me fix a pip install error in my virtual environment

Write a script that sends a message to Claude

The Claude Messages API works like a conversation. You send a list of messages (with roles like "user" and "assistant") and Claude sends back a reply. For this first call, you'll send a single question and print what comes back.

  • Create a new file called chatbot.py in your project folder. In VS Code, click **File** in the top menu bar, then **New File**.
  • Save it immediately as chatbot.py by pressing **Cmd+S** (macOS) or **Ctrl+S** (Windows).
  • Add the imports and the create_client() function by copying this code into chatbot.py:
import anthropic
import sys


def create_client():
    """Create and return an Anthropic client."""
    return anthropic.Anthropic()

What does this code do?

import anthropic loads the SDK you just installed. import sys loads Python's built-in system module, which you'll use in later steps.

The create_client() function creates an Anthropic client object. This client automatically reads your ANTHROPIC_API_KEY environment variable to authenticate, so you never paste secrets into your code.

  • Now add the main() function below create_client(). This is where the actual API call happens:
def main():
    """Send a single message to Claude and print the response."""
    client = create_client()

    # Send a single message to Claude Haiku 4.5
    response = client.messages.create(
        model="claude-haiku-4-5",
        max_tokens=1024,
        messages=[
            {"role": "user", "content": "Hello! Can you explain what a Python list is?"}
        ],
    )

    # Print Claude's response text
    print(response.content[0].text)


if __name__ == "__main__":
    main()

What does this code do?

  • client.messages.create() sends your message to the Claude Messages API and waits for a response.
  • model="claude-haiku-4-5" specifies Claude Haiku 4.5, the fastest and cheapest Claude model.
  • max_tokens=1024 sets the maximum length of Claude's reply (roughly 750 words).
  • messages is a list of conversation turns. Each turn has a role ("user" or "assistant") and content (the text).
  • response.content[0].text extracts the text from Claude's reply.
  • if __name__ == "__main__" ensures main() only runs when you execute the file directly.
  • Save chatbot.py by pressing **Cmd+S** (macOS) or **Ctrl+S** (Windows).

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

Here's the complete chatbot.py file at this point:

import anthropic
import sys


def create_client():
    """Create and return an Anthropic client."""
    return anthropic.Anthropic()


def main():
    """Send a single message to Claude and print the response."""
    client = create_client()

    response = client.messages.create(
        model="claude-haiku-4-5",
        max_tokens=1024,
        messages=[
            {"role": "user", "content": "Hello! Can you explain what a Python list is?"}
        ],
    )

    print(response.content[0].text)


if __name__ == "__main__":
    main()
Run the script and verify the response
  • Run your script by entering this command in the terminal:
python3 chatbot.py

After a brief pause (usually 1-2 seconds), you should see Claude's explanation of Python lists printed in your terminal. The exact wording will vary each time since Claude generates fresh responses.

What should I see?

You'll see a multi-paragraph explanation of Python lists. It might cover what lists are, how to create them, and basic operations like appending and indexing. The response will be different every time you run the script.

Seeing an authentication error?

If you see an error about an invalid API key or missing authentication, your ANTHROPIC_API_KEY environment variable may not be set in this terminal session. Re-run the export command from the previous step in the same terminal window where you're running the script.

If you see a connection error, check that your internet connection is working and try again.

help me fix an Anthropic API authentication error in Python

Why does the SDK find your key automatically?

When you call anthropic.Anthropic() with no arguments, the SDK looks for an environment variable named ANTHROPIC_API_KEY and uses it for authentication. This pattern keeps secrets out of your source code.

You just made your first successful API call to Claude. Your script works, but it can only handle one hardcoded question. Next up, you'll turn this into an interactive conversation loop where you type questions and Claude responds in real time.

Add Multi-Turn Conversation

Your chatbot currently sends one hardcoded question and exits. Real chatbots carry on a conversation, remembering what was said earlier and building on it.

In this step, you'll refactor your script into an interactive loop that reads your input, sends it to Claude along with the full conversation history, and prints the response.

In this step, get ready to:
  • Refactor main() into a continuous input loop.
  • Build a conversation history list that grows with each exchange.
  • Test that Claude remembers context from earlier messages.
Refactor the script into an interactive loop

Your current main() function sends a single hardcoded question and prints the answer. You need a loop that continuously reads your input, sends it to Claude, and waits for your next message.

  • In chatbot.py, select the entire main() function (from def main(): down to print(response.content[0].text)) and delete it.
  • Replace it with this new interactive version:
def main():
    """Run the interactive chatbot loop."""
    client = create_client()
    conversation_history = []

    print("🤖 Coding Tutor Chatbot")
    print("Type your questions about programming. Type 'quit' to exit.")
    print("-" * 50)

    while True:
        try:
            user_input = input("\nYou: ").strip()
        except (EOFError, KeyboardInterrupt):
            print("\n\nGoodbye! Happy coding! 🎉")
            break

        if not user_input:
            continue

        if user_input.lower() in ("quit", "exit", "q"):
            print("\nGoodbye! Happy coding! 🎉")
            print(f"Total messages in this session: {len(conversation_history) // 2}")
            break

        # Add the user's message to the conversation history
        conversation_history.append({"role": "user", "content": user_input})

        # Send the FULL history to Claude on every request
        response = client.messages.create(
            model="claude-haiku-4-5",
            max_tokens=1024,
            messages=conversation_history,
        )

        # Extract and display the assistant's response
        assistant_message = response.content[0].text
        print(f"\nTutor: {assistant_message}")
        conversation_history.append({"role": "assistant", "content": assistant_message})

What does this code do?

  • conversation_history = [] creates an empty list that will store every message exchanged between you and Claude.
  • while True starts an infinite loop that keeps the chatbot running until you choose to quit.
  • input("\nYou: ") waits for you to type something, and .strip() removes any extra whitespace.
  • The try/except block catches Ctrl+C or unexpected input closure so the chatbot exits cleanly.
  • Every time you send a message, it gets appended to conversation_history before calling the API. Claude's response is also appended so the full conversation grows with each turn.
Understand how conversation history works

The Messages API is stateless. Claude does not remember anything from previous requests on its own.

Why send the full history every time?

Each API call is completely independent. Claude has no memory between requests.

Your conversation_history list is what gives Claude the illusion of memory. By sending the entire list of past messages on every turn, Claude can read what was said before and respond in context.

This is the same pattern behind every AI chatbot, from ChatGPT to GitHub Copilot. Your app manages the memory, not the model.

  • Save chatbot.py.
Test that Claude remembers context

The best way to confirm conversation memory is working is to ask a follow-up question that only makes sense if Claude remembers your previous message.

  • Run your chatbot by entering this in the terminal:
python3 chatbot.py
  • When you see the You: prompt, type a question like:
What is a Python dictionary?
  • After Claude responds, ask a follow-up that references the first answer:
Can you show me an example of one?
  • Claude should respond with a dictionary example, proving it remembers you were talking about dictionaries.
  • Type quit to exit the chatbot.

What should I see?

After you type quit, you should see a goodbye message and a count of how many messages were exchanged. For example, if you asked two questions, it shows Total messages in this session: 2.

Claude not remembering context?

  • Make sure conversation_history.append() is called for BOTH the user message and the assistant response. Missing either one breaks the chain.
  • Check that you pass messages=conversation_history in the API call, not a hardcoded list like the old version.
  • If you see a connection error, confirm your virtual environment is still active and your ANTHROPIC_API_KEY is set in this terminal session.

Help me debug my conversation history.

✔️ Awesome, I've got everything!

Great. Double check you have saved chatbot.py and that your test conversation worked.

ⓧ I'd like to double check the full code

import anthropic
import sys


def create_client():
    """Create and return an Anthropic client."""
    return anthropic.Anthropic()


def main():
    """Run the interactive chatbot loop."""
    client = create_client()
    conversation_history = []

    print("🤖 Coding Tutor Chatbot")
    print("Type your questions about programming. Type 'quit' to exit.")
    print("-" * 50)

    while True:
        try:
            user_input = input("\nYou: ").strip()
        except (EOFError, KeyboardInterrupt):
            print("\n\nGoodbye! Happy coding! 🎉")
            break

        if not user_input:
            continue

        if user_input.lower() in ("quit", "exit", "q"):
            print("\nGoodbye! Happy coding! 🎉")
            print(f"Total messages in this session: {len(conversation_history) // 2}")
            break

        conversation_history.append({"role": "user", "content": user_input})

        response = client.messages.create(
            model="claude-haiku-4-5",
            max_tokens=1024,
            messages=conversation_history,
        )

        assistant_message = response.content[0].text
        print(f"\nTutor: {assistant_message}")
        conversation_history.append({"role": "assistant", "content": assistant_message})


if __name__ == "__main__":
    main()

Your chatbot now carries on real conversations and remembers everything you've discussed. Next up, you'll make the responses stream in real time so they appear word-by-word instead of all at once.

Stream Responses in Real Time

Right now, your chatbot waits for Claude to generate the entire response before printing anything. For short answers this is fine, but longer explanations create an awkward pause where nothing appears on screen.

Streaming fixes this by printing each token as it arrives, so text appears word-by-word in real time. This is the same technique that ChatGPT, Claude.ai, and every modern AI product uses to feel responsive.

In this step, get ready to:
  • Switch from the standard API call to the streaming endpoint.
  • Print each token as it arrives for a word-by-word effect.
  • Reconstruct the full response so conversation history still works.
Write the streaming response function

The Anthropic Python SDK provides a client.messages.stream() context manager specifically for streaming. Inside it, stream.text_stream yields each text chunk as soon as it arrives from the API.

  • In chatbot.py, add the following function between create_client() and main():
def stream_response(client, messages, system_prompt):
    """Stream a response from Claude and return the full text."""
    full_response = ""

    with client.messages.stream(
        model="claude-haiku-4-5",
        max_tokens=1024,
        system=system_prompt,
        messages=messages,
    ) as stream:
        for text in stream.text_stream:
            print(text, end="", flush=True)
            full_response += text

    print()
    return full_response

What does this code do?

client.messages.stream() opens a streaming connection to Claude. Instead of waiting for the full response, it starts yielding text chunks immediately.

print(text, end="", flush=True) prints each chunk without adding a newline, and flush=True forces it to appear on screen immediately instead of waiting for the buffer to fill.

full_response += text accumulates every chunk so the function can return the complete message at the end. You need this to store the response in conversation history.

You won't see a change yet. The next section updates main() to call this function.

Update main() to use streaming

Now you need to wire the new function into your chatbot loop. This replaces the old client.messages.create() call with your streaming function.

  • In the main() function, find this line:
    client = create_client()
    conversation_history = []
  • Add a system_prompt variable between them, so the three lines look like this:
    client = create_client()
    system_prompt = ""
    conversation_history = []

Why is the system prompt empty?

You are preparing the interface now but will fill in the actual system prompt in the next step. This keeps your code working while you build incrementally.

  • Now scroll down inside the while True loop and find this block of code:
        response = client.messages.create(
            model="claude-haiku-4-5",
            max_tokens=1024,
            messages=conversation_history,
        )

        assistant_message = response.content[0].text
        print(f"\nTutor: {assistant_message}")
        conversation_history.append({"role": "assistant", "content": assistant_message})
  • Replace that entire block with the following:
        print("\nTutor: ", end="")
        assistant_message = stream_response(client, conversation_history, system_prompt)
        conversation_history.append({"role": "assistant", "content": assistant_message})

What changed here?

print("\nTutor: ", end="") prints the label without a newline, so the streamed text flows right after it on the same line.

stream_response() handles printing each token as it arrives AND returns the full message. You store that returned message in conversation_history just like before, so Claude still remembers the full conversation.

  • Save chatbot.py.
  • Run the chatbot by entering this command in your terminal:
python3 chatbot.py
  • Ask a question like Explain what a for loop does and watch the response appear word-by-word instead of all at once.
  • Type quit to exit when you are done testing.

Response still appearing all at once?

  • Make sure you replaced the entire old block (all 8 lines from response = client.messages.create through the conversation_history.append line). If any of the old code remains, Python may run that instead.
  • Check that you saved the file after making changes. Unsaved edits do not affect the running script.
  • Verify your indentation matches exactly. The three new lines should be indented 8 spaces (inside the while True loop).

Why isn't my streaming output working?

✔️ Awesome, I've got everything!

Great. Double check you have saved chatbot.py and confirmed that streaming works.

ⓧ I'd like to double check the full code

Here is the complete chatbot.py file at this point:

import anthropic
import sys


def create_client():
    """Create and return an Anthropic client."""
    return anthropic.Anthropic()


def stream_response(client, messages, system_prompt):
    """Stream a response from Claude and return the full text."""
    full_response = ""

    with client.messages.stream(
        model="claude-haiku-4-5",
        max_tokens=1024,
        system=system_prompt,
        messages=messages,
    ) as stream:
        for text in stream.text_stream:
            print(text, end="", flush=True)
            full_response += text

    print()
    return full_response


def main():
    """Run the interactive chatbot loop."""
    client = create_client()
    system_prompt = ""
    conversation_history = []

    print("🤖 Coding Tutor Chatbot")
    print("Type your questions about programming. Type 'quit' to exit.")
    print("-" * 50)

    while True:
        try:
            user_input = input("\nYou: ").strip()
        except (EOFError, KeyboardInterrupt):
            print("\n\nGoodbye! Happy coding! 🎉")
            break

        if not user_input:
            continue

        if user_input.lower() in ("quit", "exit", "q"):
            print("\nGoodbye! Happy coding! 🎉")
            print(f"Total messages in this session: {len(conversation_history) // 2}")
            break

        conversation_history.append({"role": "user", "content": user_input})

        print("\nTutor: ", end="")
        assistant_message = stream_response(client, conversation_history, system_prompt)
        conversation_history.append({"role": "assistant", "content": assistant_message})


if __name__ == "__main__":
    main()

Your chatbot now feels like a real AI product. Responses flow naturally instead of appearing in one block. Next up, you will add a system prompt that shapes Claude into a focused coding tutor and add error handling to make the chatbot production-ready.

Add a System Prompt and Error Handling

Your chatbot streams responses and remembers conversation history. Right now, though, it has no personality. It will answer questions about anything, which is not ideal for a focused coding tutor.

In this step, you will give Claude a system prompt that defines its behavior, then add error handling so your chatbot recovers gracefully from API failures instead of crashing.

In this step, get ready to:
  • Add a system prompt that shapes Claude into a coding tutor with guardrails.
  • Wrap API calls in error handling for rate limits, network errors, and API errors.
Create the system prompt function

A system prompt is a special instruction sent with every API request that defines how the model should behave. It is separate from the conversation messages and sits at the top level of the API call.

  • In chatbot.py, find the create_client() function. Add the following new function directly below it (above stream_response):
def get_system_prompt():
    """Return the system prompt that shapes Claude into a coding tutor."""
    return """You are a friendly and patient coding tutor. Your role is to help learners understand programming concepts through clear explanations and examples.

Rules:
- Keep explanations concise but thorough
- Use code examples when helpful
- If asked about non-coding topics, politely redirect to programming
- Encourage the learner and celebrate their progress
- Ask clarifying questions when the learner's question is ambiguous"""

What does this system prompt do?

The system prompt gives Claude a consistent personality and set of rules across every message. It acts as instructions the user never sees.

  • The opening line establishes the persona: a friendly, patient coding tutor.
  • The rules list constrains Claude's behavior. "Politely redirect" means Claude will stay on-topic instead of answering random trivia.
  • "Ask clarifying questions" helps Claude give better answers instead of guessing what the learner meant.

Now you need to wire the function into main() so it actually gets used.

  • In the main() function, find this line:
    system_prompt = ""
  • Replace it with a call to your new function, so the line looks like this:
    system_prompt = get_system_prompt()
  • Save chatbot.py.
  • Run the chatbot by running:
python3 chatbot.py
  • Try asking a non-coding question like "What's the best pizza topping?" to test the guardrails. Claude should politely redirect you to programming topics.
  • Type quit to exit the chatbot.

Claude still answers non-coding questions?

Make sure you replaced the entire line system_prompt = "" with system_prompt = get_system_prompt(). If the empty string is still assigned, the system prompt is never sent.

Also check that your get_system_prompt() function is defined outside of main(), between create_client() and stream_response().

Why isn't my system prompt changing Claude's behavior?

Add error handling around API calls

Right now, if the API returns an error, your chatbot crashes. In production, APIs fail for many reasons: rate limits, network outages, or invalid requests. Wrapping the call in error handling keeps the conversation alive.

  • In chatbot.py, find these two lines near the bottom of the while True loop (right after print("\nTutor: ", end="")):
        assistant_message = stream_response(client, conversation_history, system_prompt)
        conversation_history.append({"role": "assistant", "content": assistant_message})
  • Replace those two lines with the following try/except block:
        try:
            assistant_message = stream_response(
                client, conversation_history, system_prompt
            )
            conversation_history.append(
                {"role": "assistant", "content": assistant_message}
            )
        except anthropic.RateLimitError:
            print("⚠️  Rate limited. Please wait a moment and try again.")
            conversation_history.pop()
        except anthropic.APIConnectionError:
            print("⚠️  Connection error. Check your internet and try again.")
            conversation_history.pop()
        except anthropic.APIStatusError as e:
            print(f"⚠️  API error ({e.status_code}): {e.message}")
            conversation_history.pop()

What does this error handling do?

  • anthropic.RateLimitError fires when you send too many requests too quickly. The chatbot tells you to wait instead of crashing.
  • anthropic.APIConnectionError fires when the request cannot reach the API. This usually means a network issue.
  • anthropic.APIStatusError catches all other HTTP errors (like invalid requests or server issues) and prints the status code.
  • conversation_history.pop() removes the user message that failed. This prevents a broken message from polluting future requests.
  • Save chatbot.py.
  • Run the chatbot again by running:
python3 chatbot.py
  • Ask a coding question like "How do Python lists work?" to confirm streaming still works normally.
  • Type quit to exit. You should see a session summary showing the total number of messages exchanged.

Seeing a syntax error when you run the script?

The most common cause is incorrect indentation inside the try block. The try: line must be at the same indentation level as the old assistant_message = ... line (8 spaces inside the while loop).

Also check that each except line aligns with try: and that the code inside each block is indented one more level.

Help me fix a syntax error in my try/except block for Anthropic API error handling.

Why pop the user message on error?

The Claude Messages API is stateless. You send the full conversation history with every request. If a failed user message stays in the list, it gets sent again on the next attempt without a matching assistant response. This violates the API's alternating user/assistant message requirement.

✔️ Awesome, I've got everything!

Great. Double check you have saved chatbot.py.

ⓧ I'd like to double check the full code

Here is the complete chatbot.py file. Compare it against yours line by line.

import anthropic
import sys


def create_client():
    """Create and return an Anthropic client."""
    return anthropic.Anthropic()


def get_system_prompt():
    """Return the system prompt that shapes Claude into a coding tutor."""
    return """You are a friendly and patient coding tutor. Your role is to help learners understand programming concepts through clear explanations and examples.

Rules:
- Keep explanations concise but thorough
- Use code examples when helpful
- If asked about non-coding topics, politely redirect to programming
- Encourage the learner and celebrate their progress
- Ask clarifying questions when the learner's question is ambiguous"""


def stream_response(client, messages, system_prompt):
    """Stream a response from Claude and return the full text."""
    full_response = ""

    with client.messages.stream(
        model="claude-haiku-4-5",
        max_tokens=1024,
        system=system_prompt,
        messages=messages,
    ) as stream:
        for text in stream.text_stream:
            print(text, end="", flush=True)
            full_response += text

    print()
    return full_response


def main():
    """Run the interactive chatbot loop."""
    client = create_client()
    system_prompt = get_system_prompt()
    conversation_history = []

    print("\U0001f916 Coding Tutor Chatbot")
    print("Type your questions about programming. Type 'quit' to exit.")
    print("-" * 50)

    while True:
        try:
            user_input = input("\nYou: ").strip()
        except (EOFError, KeyboardInterrupt):
            print("\n\nGoodbye! Happy coding! \U0001f389")
            break

        if not user_input:
            continue

        if user_input.lower() in ("quit", "exit", "q"):
            print("\nGoodbye! Happy coding! \U0001f389")
            print(f"Total messages in this session: {len(conversation_history) // 2}")
            break

        conversation_history.append({"role": "user", "content": user_input})

        print("\nTutor: ", end="")

        try:
            assistant_message = stream_response(
                client, conversation_history, system_prompt
            )
            conversation_history.append(
                {"role": "assistant", "content": assistant_message}
            )
        except anthropic.RateLimitError:
            print("\u26a0\ufe0f  Rate limited. Please wait a moment and try again.")
            conversation_history.pop()
        except anthropic.APIConnectionError:
            print("\u26a0\ufe0f  Connection error. Check your internet and try again.")
            conversation_history.pop()
        except anthropic.APIStatusError as e:
            print(f"\u26a0\ufe0f  API error ({e.status_code}): {e.message}")
            conversation_history.pop()


if __name__ == "__main__":
    main()

Your chatbot is complete. It streams responses, remembers context, stays on topic, and handles errors gracefully. This is the same pattern powering production AI applications everywhere.

Secret mission

Add Token Counting and Cost Tracking

Every API call costs money. Right now your chatbot gives you no visibility into how much you're spending. Build a real-time cost dashboard that shows token usage and estimated cost after every single response.

Clean Up Your Resources

Clean Up Your Resources

This project uses the Anthropic API which charges per token. You are not charged for idle credits. There are no ongoing costs unless you actively make API requests.

Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • An Anthropic Console account with API credits ($5 minimum purchase).
  • A Python virtual environment with the anthropic package installed.
  • Your project folder containing chatbot.py on your Desktop.
  • The ANTHROPIC_API_KEY environment variable in your terminal session.

Keep everything running

No action needed. Choose this if you plan to keep building with the Claude API.

  • Your API credits remain available for future calls. You are only charged when you send requests.
  • Your project folder and virtual environment stay on your Desktop, ready to use.
  • Your ANTHROPIC_API_KEY environment variable will persist if you added it to your shell profile (~/.zshrc or ~/.bashrc). Otherwise it will disappear when you close the terminal.

Pause - I'll come back to this later

Nothing to pause. Your credits sit idle until you make API requests. No charges accrue while the project is unused.

  • Credits expire 1 year from purchase date, so you have time to come back.
  • Your project folder and virtual environment will stay on your Desktop until you delete them manually.

Delete - I don't want to use this again

Remove all project resources and revoke API access. Credits are non-refundable but will expire after 1 year if unused.

  • Delete your API key: open the Claude Console at console.anthropic.com, navigate to Account Settings, find your key, and delete it.
  • Remove the ANTHROPIC_API_KEY environment variable from your current session by running:
unset ANTHROPIC_API_KEY
  • If you added the export line to your shell profile (~/.zshrc or ~/.bashrc), open that file and delete the export ANTHROPIC_API_KEY=... line.
  • Delete your project folder by running the command for your operating system:

macOS

rm -rf ~/Desktop/chatbot-project

Windows

rmdir /s /q %USERPROFILE%\Desktop\chatbot-project

Nice Work!

Nice Work!

You just built an AI chatbot from scratch using the Claude Messages API. This is the exact pattern behind every AI product, from customer support bots to coding assistants.

You've learned how to:

  • Built a working chatbot that calls the Claude Messages API and maintains full conversation history across multiple turns.
  • Added real-time streaming so responses appear word-by-word in the terminal, just like production AI apps.
  • Crafted a system prompt that shapes Claude into a focused coding tutor with guardrails and personality.
  • Built a token counter and cost tracker to monitor API spend in real time.

Ready to quiz yourself?