In this lesson: Write a working call, extract the text safely, and read the token usage.
Here is the whole thing, and then the parts that matter.
import anthropic
client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY from the environment
response = client.messages.create(
model="claude-opus-5",
max_tokens=16000,
system="You are a careful assistant. If you are unsure, say so.",
messages=[
{"role": "user", "content": "Explain gradient descent in three sentences."}
],
)
for block in response.content:
if block.type == "text":
print(block.text)
print(response.usage.input_tokens, response.usage.output_tokens)
The client
anthropic.Anthropic() with no arguments resolves the key from the environment. Passing the key as a literal in code is the single most common way secrets end up in a public repository — do not do it, not even temporarily.
system versus messages
system is a separate top-level parameter, not a message with role="system". It holds standing instructions — persona, rules, output policy — and stays constant while messages changes. Keeping it separate is also what makes caching work later.
max_tokens is a ceiling on the reply
It limits output only, and the model does not know about it. Set it too low and answers are cut off mid-sentence with stop_reason == "max_tokens". Sensible defaults: about 16000 for ordinary non-streaming requests, around 256 for classification, and much larger with streaming when you genuinely need long output.
Reading the response safely
response.content[0].text. The first block may be a thinking block or a tool call, and your code will crash the first time the model reasons. Filter by type — this habit costs nothing now and saves a production incident later.
text = "".join(b.text for b in response.content if b.type == "text")
Always check stop_reason
| stop_reason | Meaning | What your code should do |
|---|---|---|
end_turn | Finished normally | Use the text |
max_tokens | Hit your ceiling | Raise the limit or ask for less |
tool_use | Wants to call a tool | Run it and continue the loop |
refusal | Declined on safety grounds | Handle it — do not read the text and hope |
Multi-turn conversation
Because the API is stateless, you keep the list and append to it:
messages = []
def ask(question: str) -> str:
messages.append({"role": "user", "content": question})
response = client.messages.create(
model="claude-opus-5", max_tokens=16000, messages=messages,
)
messages.append({"role": "assistant", "content": response.content})
return "".join(b.text for b in response.content if b.type == "text")
Note that the assistant turn is appended as response.content — the block list, not the extracted string. Appending only the text quietly discards tool calls and thinking, and breaks the next turn.
Errors are typed — catch specifically
try:
response = client.messages.create(...)
except anthropic.RateLimitError:
... # back off and retry
except anthropic.APIConnectionError:
... # network — retry
except anthropic.BadRequestError:
... # your request is wrong; retrying will not help
except anthropic.APIError as e:
... # everything else
One broad except loses the distinction between "retry in two seconds" and "this will fail forever". The SDK already retries connection errors, 429 and 5xx a couple of times on its own.
Try it yourself
Write a script that asks the same question at max_tokens=50 and at max_tokens=2000, and prints stop_reason and both token counts each time. Seeing max_tokens appear in the first case makes the ceiling concrete in a way that reading about it does not.