Read a Response
What messages.create() Returns
When you call client.messages.create(...), the SDK returns a Message object. It is a typed Python object that represents Claude's complete reply and everything the API reported about it.
from anthropic import Anthropic
client = Anthropic()
message = client.messages.create(
model="claude-opus-5-5",
max_tokens=1024,
messages=[{"role": "user", "content": "Name three Python web frameworks."}],
)
print(type(message))
<class 'anthropic.types.message.Message'>
Reading a response correctly means more than printing the text. A robust application also checks why Claude stopped, how many tokens were used, and which request produced the reply. This lesson explains every part of the Message object and how to use each one.
The Structure of a Response
Converted to JSON, a typical response looks like this. Values will differ for you, and some optional fields are omitted.
{
"id": "msg_01XFDUDYJgAACzvnptvVoYEL",
"type": "message",
"role": "assistant",
"model": "claude-opus-5-5",
"content": [
{
"type": "thinking",
"thinking": "",
"signature": "EuYBCkQYAiJA..."
},
{
"type": "text",
"text": "Three popular Python web frameworks are Django, Flask, and FastAPI."
}
],
"stop_reason": "end_turn",
"stop_sequence": null,
"stop_details": null,
"usage": {
"input_tokens": 15,
"output_tokens": 88,
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 0
}
}
The fields fall into four groups:
| Group | Fields | Answers the question |
|---|---|---|
| Identity | id, type, role, model | What is this, and who produced it? |
| Content | content | What did Claude say? |
| Completion | stop_reason, stop_sequence, stop_details | Why did Claude stop, and is the answer complete? |
| Usage | usage | How many tokens did this cost? |
Identity Fields
print(message.id) # msg_01XFDUDYJgAACzvnptvVoYEL
print(message.type) # message
print(message.role) # assistant
print(message.model) # claude-opus-5-5
| Field | Meaning | Practical use |
|---|---|---|
id | A unique identifier for this message, starting with msg_ | Store it with your records and logs to refer to a specific reply |
type | Always "message" for a successful Messages API response | Distinguishing object types when processing mixed data |
role | Always "assistant", because the reply comes from Claude | Appending the reply to a conversation history (Lesson 24) |
model | The model that generated the reply | Confirming which model answered, especially when the model is configurable |
The Content Field
message.content is a list of content blocks. Each block has a type that tells you what kind of content it is:
for block in message.content:
print(block.type)
thinking
text
The two block types you will see in ordinary requests are:
| Block type | Contains | Show it to users? |
|---|---|---|
text | Claude's answer, in block.text | Yes |
thinking | Claude's internal reasoning. The thinking text is empty by default; a signature field is also present. | No, unless you deliberately request and display summarized thinking |
Other block types appear when you use tools and other features. Lesson 16 covers all of them.
Reading the Text
Read text by type, never by position:
answer = "".join(block.text for block in message.content if block.type == "text")
print(answer)
Why not message.content[0].text? On Claude Opus 5.5, thinking is always on, so the first block is often a thinking block, which has no text attribute. Position-based access fails intermittently, depending on whether the model chose to think. Type-based access always works (Lesson 11).
Completion Fields: Is the Answer Complete?
stop_reason
stop_reason explains why generation ended. Check it before using the content.
| Value | Meaning | Is the content a complete answer? |
|---|---|---|
end_turn | Claude finished naturally | Yes |
max_tokens | The max_tokens limit was reached | No: truncated (Lesson 13) |
stop_sequence | One of your custom stop sequences appeared | Yes, ending where you asked (Lesson 48) |
tool_use | Claude wants your application to run a tool | Not yet: your code must act (Lesson 85) |
pause_turn | A long server-side tool operation paused | Not yet: send the content back to continue |
refusal | Claude declined the request | No: do not treat it as a normal answer |
model_context_window_exceeded | The context window filled up | No: truncated |
Lesson 20 (Handle Stop Reasons) shows how to handle each value.
stop_sequence
When stop_reason is "stop_sequence", this field contains the exact sequence that ended generation. Otherwise, it is None.
stop_details
When stop_reason is "refusal", stop_details describes the refusal, for example the policy category that triggered it. For every other stop reason, it is None.
if message.stop_reason == "refusal":
print("Refused:", message.stop_details)
Usage Fields: What Did It Cost?
message.usage reports token counts for this request:
usage = message.usage
print(usage.input_tokens) # Tokens in your request
print(usage.output_tokens) # Tokens generated, including thinking
print(usage.cache_creation_input_tokens) # Tokens written to the prompt cache
print(usage.cache_read_input_tokens) # Tokens read from the prompt cache
| Field | Meaning |
|---|---|
input_tokens | Input tokens processed normally: your system prompt, messages, and so on |
output_tokens | All generated tokens, including thinking tokens that are not shown |
cache_creation_input_tokens | Input tokens written to the prompt cache (Lessons 121–130) |
cache_read_input_tokens | Input tokens read from the cache, which are billed at a much lower rate |
Some features add further fields. For example, server tools report how many times they were used.
Practical use: multiply these counts by your model's prices to get the exact cost of the request (Lesson 12). Logging usage for every request is the foundation of cost monitoring (Lesson 186).
The Request ID
Every response carries the ID the API assigned to the HTTP request:
print(message._request_id) # req_018EeWyXxfu5pfWkrYcMdjWG
message.id | message._request_id | |
|---|---|---|
| Identifies | The message (Claude's reply) | The HTTP request that produced it |
| Prefix | msg_ | req_ |
| Main use | Referring to replies in your application | Support tickets and debugging with Anthropic |
Although _request_id starts with an underscore, it is a public, documented property.
Converting a Response
To a Dictionary or JSON
data = message.to_dict() # Python dict
text = message.to_json() # JSON-formatted string (indented by default)
When to use them:
to_dict()when you need plain data, for example to return from a web API, store in a document database, or compare in tests.to_json()for logging, saving to a file, or reading the full structure while debugging.
Missing Fields vs. None
Some fields can be absent from a response, or present with the value null. Both appear as None in Python. When the difference matters, check model_fields_set:
if message.stop_sequence is None:
if "stop_sequence" in message.model_fields_set:
print("The API returned stop_sequence = null")
else:
print("The API did not include stop_sequence at all")
model_fields_set is a Pydantic feature. It lists the fields that were actually present in the JSON the API returned. Most applications never need this distinction, but it is useful when debugging unexpected data.
Reading HTTP Headers with with_raw_response
The Message object contains the response body. To read HTTP headers as well, such as rate-limit information or the workspace that handled the request, use .with_raw_response:
raw = client.messages.with_raw_response.create(
model="claude-opus-5-5",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello"}],
)
print(raw.headers.get("request-id"))
print(raw.headers.get("anthropic-workspace-id"))
message = raw.parse() # The same Message object that create() would have returned
with_raw_responsereturns anAPIResponsewrapper instead of theMessage.raw.headersholds the HTTP response headers.raw.parse()gives you the usual typedMessage.
Lesson 159 (Read Raw Responses) covers this in depth.
Practical Example: A Response Inspector
During development, it helps to see every part of a response in a readable form. This utility prints a structured report for any Message.
"""inspect_response.py: print a readable report of a Message object."""
from anthropic import Anthropic
from anthropic.types import Message
COMPLETE_REASONS = {"end_turn", "stop_sequence"}
def get_text(message: Message) -> str:
return "".join(block.text for block in message.content if block.type == "text")
def inspect(message: Message) -> None:
usage = message.usage
complete = message.stop_reason in COMPLETE_REASONS
print("=" * 60)
print(f"Message ID : {message.id}")
print(f"Request ID : {message._request_id}")
print(f"Model : {message.model}")
print(f"Blocks : {[block.type for block in message.content]}")
print(f"Stop reason: {message.stop_reason}" + ("" if complete else " <-- check this"))
if message.stop_details is not None:
print(f"Details : {message.stop_details}")
print(f"Tokens : in={usage.input_tokens}, out={usage.output_tokens}")
print("-" * 60)
print(get_text(message) or "(no text in this response)")
print("=" * 60)
if __name__ == "__main__":
client = Anthropic()
reply = client.messages.create(
model="claude-opus-5-5",
max_tokens=1024,
messages=[{"role": "user", "content": "Name three Python web frameworks, one line each."}],
)
inspect(reply)
Example output:
============================================================
Message ID : msg_01XFDUDYJgAACzvnptvVoYEL
Request ID : req_018EeWyXxfu5pfWkrYcMdjWG
Model : claude-opus-5-5
Blocks : ['thinking', 'text']
Stop reason: end_turn
Tokens : in=19, out=96
------------------------------------------------------------
Django: a full-featured framework with an ORM, admin, and authentication built in.
Flask: a lightweight microframework you extend with libraries as needed.
FastAPI: a modern async framework with automatic validation and API docs.
============================================================
How It Works
COMPLETE_REASONSdefines which stop reasons mean "this is a finished answer". Any other value gets a visible marker, so problems such as truncation stand out immediately.Blocksshows the block types, which makes thinking blocks and tool calls visible while you learn.stop_detailsis printed only when present, which is when Claude refused.get_text(message) or "(no text...)"prints a clear placeholder when there is no text, for example when a smallmax_tokenswas used up by thinking. An empty line would be confusing.- Both IDs are shown, because you will need the request ID if you ever contact support.
Common Mistakes
Treating the Response Like a Dictionary
message["content"] # TypeError: 'Message' object is not subscriptable
Fix: use attributes, as in message.content, or convert with message.to_dict() first.
Reading Text by Position
What happens: message.content[0].text raises AttributeError whenever the first block is a thinking block.
Fix: filter blocks by type.
Ignoring stop_reason
What happens: truncated answers, refusals, and pending tool calls are treated as normal, complete answers.
Fix: check stop_reason first, every time it matters.
Estimating Cost from the Visible Text
What happens: costs are underestimated, because thinking tokens are counted in output_tokens but not visible.
Fix: always use message.usage.
Confusing id and _request_id
What happens: support cannot locate the request because the message ID was provided instead of the request ID.
Fix: log both, and give support the req_... value.
Best Practices
- Check
stop_reasonbefore using content. - Extract text by block type with a helper function.
- Log
id,_request_id,model,stop_reason, andusagefor every request in production. - Use
to_dict()orto_json()when storing or returning responses, neverstr(message). - Keep response handling in one place, such as a helper or service module, so changes to response handling are made once.
Exercise
- Send any request and print
message.to_json(). Identify each of the four field groups. - Use the
inspect()function on a request withmax_tokens=40and a question that needs a long answer. What does the inspector show? - Write a function
summarize_usage(message)that returns a string like"in=19 out=96 total=115". - Use
with_raw_responseto print therequest-idheader, and confirm that it matchesmessage._request_idfrom the parsed message.
Step 2: the stop reason is max_tokens, marked with <-- check this. The text is either cut off, or replaced by (no text in this response) if thinking used the whole budget.
Step 3:
def summarize_usage(message) -> str:
usage = message.usage
total = usage.input_tokens + usage.output_tokens
return f"in={usage.input_tokens} out={usage.output_tokens} total={total}"
Step 4:
raw = client.messages.with_raw_response.create(
model="claude-opus-5-5",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello"}],
)
message = raw.parse()
print(raw.headers.get("request-id"))
print(message._request_id) # The same value