Porting a Chat Loop From OpenAI to Claude in Python: One Page of Changes
Move a stateless message loop to Anthropic's Messages API: the top-level system parameter, required max_tokens, content blocks and strict role alternation.
This article is published in English.
Sources: Part 3 — Call the Claude API in Python (same messages, system on the side)
Anyone who has written a chat loop against OpenAI can move it to Anthropic's Claude in roughly a page of changes. Both providers share the core shape: a role-tagged messages list sent in full each call, with the reply appended afterward. Only one element moves, and it helps to know exactly which.
Same list, system outside it
Claude's Messages API accepts the conversation format you already know: a list of dicts with a role and content, stateless on the server, resent in full each time. Role names and meanings are identical, user and assistant.
The one structural difference is that system isn't a message. OpenAI puts instructions in row zero of the list. Anthropic lifts them into a top-level system= parameter that travels beside the list. The list holds only the real conversation, user and assistant turns that strictly alternate.
import anthropic
client = anthropic.Anthropic() # key from env
resp = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=300,
system="You are a concise Python assistant.",
messages=[
{
"role": "user",
"content": "Why is system a parameter "
"here, not a message?",
},
],
)
print(resp.content[0].text)
Two smaller differences come up before any of that runs:
max_tokenshas to be set. OpenAI lets a request go out without an output ceiling. Claude wants one on every call, which is a little irritating but keeps cost in view, because a request can't be made until you've decided how long the reply may run.- Replies come as content blocks. Instead of one string you get a list, and plain text lives at
resp.content[0].text. It seems like overkill today, but tool calls arrive in that same structure later on, so it earns its keep.
Setup and a single turn
Install the SDK with pip install anthropic, then export the key from the Anthropic console (it begins with sk-ant-) as ANTHROPIC_API_KEY. The client picks it up on its own.
Set it beside the OpenAI call and the entire difference is four edits. The method client.chat.completions.create turns into client.messages.create, the system row moves into system=, an output cap becomes compulsory, and the text is read from content[0].text instead of choices[0].message.content. A note on the SDK: the current anthropic package doesn't accept temperature= on messages.create, so older snippets that include it will error out. Default sampling applies, and that's fine for these examples.
Token counts sit in resp.usage.input_tokens and resp.usage.output_tokens. These are the same meter with slightly different field names, and it's worth logging them every time.
The history loop on Claude
Here's the ask() function from the OpenAI version, ported. The append-and-resend logic hasn't changed. Only the call in the middle did.
msgs = []
def ask(text: str) -> str:
msgs.append(
{"role": "user", "content": text}
)
resp = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=300,
system="You are a concise assistant.",
messages=msgs, # full history
)
reply = resp.content[0].text
msgs.append(
{"role": "assistant", "content": reply}
)
return reply
print(ask("Define a context window."))
print(ask("Now for a five-year-old."))
print(ask("Which answer was shorter?"))
The third question in a conversation still proves the loop, because it works only if both earlier answers sit in msgs and get resent. The same economics apply as well. Input tokens grow with every turn, so long conversations cost more per call.
Claude is also pickier about order. Messages must alternate between user and assistant. Two user messages back to back, which happens easily when a failed request gets retried, make Claude reject the call while OpenAI tolerates it. Consider that a free lint rule for whatever manages your history.
Running it
The script runs a single turn and then a three-turn conversation, logging input and output tokens after every call. It stops early with a clear message if ANTHROPIC_API_KEY is missing. It also makes live, billed requests, so keep it out of any CI run.
pip install -r requirements.txt
export ANTHROPIC_API_KEY="sk-ant..."
python examples/part03_claude.py
Why the port was easy, and what you gain
It was a small change since both providers expose the same contract: a stateless endpoint that takes the full conversation on each call and answers once. What differs is dialect, such as where the system prompt sits, how the reply object looks and which parameters are required. LangChain abstracts that thin layer, and its ChatOpenAI and ChatAnthropic classes exist to absorb the four-line diff you just wrote manually. Knowing what the abstraction hides is the difference between using it and being trapped by it.
There's a practical benefit as well. Two providers behind one ask() shape let you A/B test models, fail over on a bad API day, or route simple questions to one and difficult ones to the other, all without changing application code. Google's Gemini API follows the same conversation shape with a single renamed role: the assistant is called model.