Python with AI Tutorial · Chapter 44 of 48
A chatbot in Python is a loop that reads what the user types, sends the whole conversation so far to an AI API, prints the reply and repeats. The model itself remembers nothing between calls, so your code keeps the history. This chapter builds a terminal chatbot with a system prompt, then moves the same logic into a small Streamlit web page.
How a chatbot actually works
An AI API call is stateless: it sees only the messages you send in that request. To make the model "remember" that the customer asked about gluten-free bread two turns ago, you resend that turn every time. A chatbot is therefore three parts: a system prompt that fixes the role, a growing list of user and assistant messages, and a loop. Everything else is decoration.
The message list
Messages are dictionaries with a role and content. Roles alternate between user and assistant; the system role carries instructions. This snippet needs no API key.
history = [
{"role": "system", "content": "You are the assistant for Crumb & Co bakery."},
{"role": "user", "content": "Do you sell gluten-free bread?"},
{"role": "assistant", "content": "Yes, we bake a gluten-free loaf every morning."},
{"role": "user", "content": "How much is it?"},
]
for m in history:
print(f"{m['role']:9} | {m['content']}")
Output: system | You are the assistant for Crumb & Co bakery. user | Do you sell gluten-free bread? assistant | Yes, we bake a gluten-free loaf every morning. user | How much is it?
One turn with a system prompt
The system prompt should contain the facts the bot may quote, because a model without them will invent prices. The Chat Completions endpoint accepts the list directly.
from openai import OpenAI
SYSTEM = (
"You are the assistant for Crumb & Co bakery. Prices: sourdough 5.50, "
"gluten-free loaf 6.50, croissant 2.20. Open 7am to 3pm, closed Monday. "
"Answer in one or two short sentences. If you do not know, say so."
)
history = [
{"role": "system", "content": SYSTEM},
{"role": "user", "content": "Do you sell gluten-free bread?"},
{"role": "assistant", "content": "Yes, we bake a gluten-free loaf every morning."},
{"role": "user", "content": "How much is it?"},
]
client = OpenAI() # reads OPENAI_API_KEY
completion = client.chat.completions.create(model="gpt-5", messages=history)
print(completion.choices[0].message.content)
Output: The gluten-free loaf is 6.50.
The terminal chatbot
Now wrap it in a loop. This version uses the Anthropic SDK, which takes the system prompt as a separate argument and the turns as messages. Save it as bakery_bot.py and run it with python bakery_bot.py.
import anthropic
SYSTEM = (
"You are the assistant for Crumb & Co bakery. Prices: sourdough 5.50, "
"gluten-free loaf 6.50, croissant 2.20. Open 7am to 3pm, closed Monday. "
"Answer in one or two short sentences. If you do not know, say so."
)
client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY
history = []
print("Bakery bot ready. Type quit to exit.")
while True:
user_text = input("You: ").strip()
if user_text.lower() in {"quit", "exit"}:
print("Bot: Bye!")
break
if not user_text:
continue
history.append({"role": "user", "content": user_text})
reply = client.messages.create(
model="claude-sonnet-5", max_tokens=400, system=SYSTEM, messages=history
)
answer = reply.content[0].text
history.append({"role": "assistant", "content": answer})
print("Bot:", answer)
Output: Bakery bot ready. Type quit to exit. You: Are you open on Monday? Bot: No, we are closed on Mondays. We open Tuesday to Sunday, 7am to 3pm. You: And what does a croissant cost? Bot: A croissant is 2.20. You: quit Bot: Bye!
Notice that the second question never mentions the bakery, yet the answer is correct, because the first exchange travelled along in history.
Stop the history from growing forever
Every turn resends the whole list, so a long chat costs more and eventually hits the context limit. Keep the last N turns. This helper is pure Python and testable without a key.
def trim(history: list[dict], max_turns: int = 10) -> list[dict]:
"""Keep only the most recent user/assistant pairs."""
return history[-2 * max_turns:]
fake = [{"role": "user" if i % 2 == 0 else "assistant", "content": f"msg {i}"} for i in range(30)]
short = trim(fake, max_turns=4)
print(len(fake), "->", len(short), "| first kept:", short[0]["content"])
Output: 30 -> 8 | first kept: msg 22
Call history = trim(history) right before the API call. The system prompt is passed separately, so it is never trimmed away.
Handle a failed call
If the network drops or you hit a rate limit, the user message is already in the list but no answer follows, which breaks the alternating pattern. Remove it on failure and let the user try again.
import anthropic
def ask(client, system, history):
try:
reply = client.messages.create(
model="claude-sonnet-5", max_tokens=400, system=system, messages=history
)
return reply.content[0].text
except anthropic.RateLimitError:
history.pop()
return "Too many requests right now, please try again in a moment."
except anthropic.APIStatusError as e:
history.pop()
return f"Service error {e.status_code}, your message was not sent."
# in the loop: answer = ask(client, SYSTEM, history)
Output: (no output on its own - drop the function into the loop above)
Save the transcript
Writing the history to JSON lets you review conversations and resume them later.
import json
from datetime import datetime
history = [
{"role": "user", "content": "Are you open on Monday?"},
{"role": "assistant", "content": "No, we are closed on Mondays."},
]
name = f"chat_{datetime.now():%Y%m%d_%H%M}.json"
with open(name, "w", encoding="utf-8") as f:
json.dump(history, f, indent=2, ensure_ascii=False)
print("saved", name, "with", len(history), "messages")
Output: saved chat_20260905_1030.json with 2 messages
Extend the terminal bot with two features and let the assistant explain each change.
Here is my Python terminal chatbot using the anthropic SDK: [paste bakery_bot.py]. Add a /reset command that clears the history, and print the running total of usage.input_tokens plus usage.output_tokens after each reply. Keep the code under 40 lines and explain each change briefly.
Move it to a web UI with Streamlit
Streamlit turns a script into a web page with no HTML. Install it with pip install streamlit, save the code below as app.py and run streamlit run app.py. The browser opens on localhost. Because Streamlit reruns the script on every interaction, the history lives in st.session_state.
import streamlit as st
from openai import OpenAI
SYSTEM = ("You are the assistant for Crumb & Co bakery. Prices: sourdough 5.50, "
"gluten-free loaf 6.50, croissant 2.20. Answer briefly.")
client = OpenAI()
st.title("Crumb & Co assistant")
if "history" not in st.session_state:
st.session_state.history = [{"role": "system", "content": SYSTEM}]
for m in st.session_state.history[1:]:
st.chat_message(m["role"]).write(m["content"])
if prompt := st.chat_input("Ask about bread, prices or opening hours"):
st.session_state.history.append({"role": "user", "content": prompt})
st.chat_message("user").write(prompt)
completion = client.chat.completions.create(
model="gpt-5", messages=st.session_state.history
)
answer = completion.choices[0].message.content
st.session_state.history.append({"role": "assistant", "content": answer})
st.chat_message("assistant").write(answer)
Output: You can now view your Streamlit app in your browser. Local URL: http://localhost:8501
The page shows chat bubbles, an input box at the bottom and the full conversation. Share it on your network with streamlit run app.py --server.address 0.0.0.0, or deploy it to Streamlit Community Cloud with the API key stored as a secret, never in the code.
Get a system prompt written for your own business before you paste in real facts.
Write a system prompt for a customer-service chatbot for a small accounting firm. It must: only answer from the facts I provide, refuse to give tax advice, offer to book a call for anything complex, and stay under 120 words. Leave placeholders for opening hours, prices and contact email.
Common mistakes
- Sending only the latest message and wondering why the bot forgets everything.
- Putting the system prompt inside the Anthropic
messageslist; it belongs in thesystemargument. - Leaving the user message in the history after a failed call, which produces two user turns in a row.
- Letting the history grow without limit in a long-running bot.
- Hard-coding the API key in
app.pyand deploying it.
Exercise
Add a /summary command to the terminal bot. When the user types it, the bot should send the conversation to the model with the instruction "Summarise this conversation in two sentences" and print the result, without adding the summary to the history.
Show answer
if user_text == "/summary":
transcript = "\n".join(f"{m['role']}: {m['content']}" for m in history)
s = client.messages.create(
model="claude-sonnet-5", max_tokens=200,
messages=[{"role": "user", "content": "Summarise this conversation in two sentences:\n" + transcript}],
)
print("Summary:", s.content[0].text)
continue
Output: You: /summary Summary: The customer asked about Monday opening hours and was told the bakery is closed that day. They then asked the croissant price, which is 2.20.
Place the block inside the loop after the empty-input check and before the message is appended to history.
Related chapters
FAQ
How does a Python chatbot remember the conversation?
It does not; your code does. Every turn appends the user message and the reply to a list, and the whole list is sent with the next request. Trim it to the last few turns to control cost.
Can I build a chatbot in Python without an API key?
You can run a small open model locally with tools such as Ollama, which exposes an OpenAI-compatible endpoint, so the same code works with a different base URL and no cloud key.
Is Streamlit good enough for a real chatbot?
For internal tools, demos and small teams, yes. For a public product you will eventually want authentication, a database for transcripts and a proper web framework.
Working with spreadsheets too? Ready-made Excel, Google Sheets and Power BI templates are at NextGenTemplates.com.
Chapter 44 of 48 · Python with AI: all 48 chapters



