// lesson: messages-format
The Messages Format
By the end of this course you will have a chatbot running in your terminal โ your API key, your machine, a real model on the other end. Each lesson adds one piece to that app, and each challenge unit-tests the one function you just wrote. The full app runs locally; the graded challenges never touch the network.
Every major LLM API speaks the same basic shape: a list of messages, where
each message is a dict with a role and a content string.
[
{"role": "system", "content": "You are a concise assistant."},
{"role": "user", "content": "What is a context window?"},
{"role": "assistant", "content": "The maximum amount of text a model..."},
]
The three roles matter:
- system โ instructions from you, the developer: persona, tone, rules. The user never types this; your app supplies it.
- user โ what the human typed.
- assistant โ what the model said back. On the next call you send it right back in, which is how the model "remembers" the conversation. The API itself is stateless โ the message list is the memory.
Let's make one real call. Create a project folder, install the SDK, and set your key (this course shows the Anthropic SDK; the OpenAI equivalent appears in the next lesson and everything in the course works with either):
mkdir chatbot && cd chatbot
python -m venv .venv && source .venv/bin/activate
pip install anthropic
export ANTHROPIC_API_KEY=sk-ant-...
Now the first cut of chatbot.py:
import anthropic
client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY
response = client.messages.create(
model="claude-opus-4-8",
max_tokens=1024,
system="You are a concise assistant.",
messages=[{"role": "user", "content": "Say hello in five words."}],
)
print(response.content[0].text)
Run it: python chatbot.py. That's a real model answering you.
max_tokens is required, and it's easy to confuse with the context window
you'll meet in the memory lesson โ they cap different things. max_tokens
limits how long this one reply can be; the model stops generating,
possibly mid-sentence, once it hits that count. The context window is the
model's hard limit on total input size across the whole conversation.
Set max_tokens too low and long answers get truncated; that's independent
of how much history you've accumulated.
One wrinkle worth noticing: Anthropic's API takes the system prompt as a
separate system= parameter, while OpenAI's API takes it as a
{"role": "system", ...} message at the front of the list. We'll keep the
system message in our history list (the portable, OpenAI-style shape) and
let a small adapter function split it out for whichever API we call. That
adapter arrives in the next lesson.
First, the piece we can test today: every conversation in our app starts from
a helper that builds the initial history. Add this to chatbot.py:
def make_conversation(system_prompt):
if system_prompt is None or not system_prompt.strip():
return []
return [{"role": "system", "content": system_prompt.strip()}]
Small, but it pins down real decisions: a blank or missing prompt means an empty history (no junk system message), stray whitespace gets stripped, and โ importantly โ every call returns a fresh list. If two conversations shared one list, mutating one chat's history would corrupt the other's. That kind of aliasing bug โ handing every caller the same mutable object instead of a fresh one โ is a Python classic; the tests below check for it.
โบ Start the Conversation
10 ptsImplement make_conversation(system_prompt):
- If
system_promptisNone, empty, or only whitespace, return[]. - Otherwise return a new list containing exactly one message:
{"role": "system", "content": <system_prompt with surrounding whitespace stripped>}. - Every call must return a fresh list โ mutating one returned list must not affect another.
Log in to submit a solution and earn points.