Get Final Messages
What a Final Message Is
A stream delivers a response in pieces. Once it finishes, you usually still need the complete response: to store it in conversation history, check the stop reason, record usage, or read tool calls. The stream helper's get_final_message() provides exactly that:
from anthropic import Anthropic
client = Anthropic()
with client.messages.stream(
model="claude-opus-5-5",
max_tokens=1024,
messages=[{"role": "user", "content": "Name three Python testing frameworks."}],
) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)
message = stream.get_final_message()
print(f"\n\nstop_reason={message.stop_reason}, output_tokens={message.usage.output_tokens}")
According to the documentation, the SDK's accumulation helper collects all events into the complete Message object, identical in structure to what client.messages.create() returns. Everything from Lessons 14–20 and 41–50 applies to it: content blocks, stop reasons, usage, and IDs.
Why You Need the Final Message
The text chunks from text_stream are only part of the response:
| Information | In text_stream chunks? | In the final message? |
|---|---|---|
| Answer text | Yes, in pieces | Yes, in text blocks |
| Thinking blocks (with signatures) | No | Yes |
| Tool calls with complete inputs | No | Yes |
| Citations | No | Yes |
stop_reason and stop_sequence | No | Yes |
usage (final, cumulative) | No | Yes |
| Message ID | No | Yes |
Most importantly, conversation history must contain the complete assistant turn, including thinking blocks and tool calls. Rebuilding it from text chunks would lose them (Lessons 16 and 24).
Pattern 1: Display, Then Get the Final Message
The common pattern for interactive applications: stream the text for the user, then retrieve the final message for your code.
with client.messages.stream(model="claude-opus-5-5", max_tokens=2048, messages=history) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)
final = stream.get_final_message()
history.append({"role": "assistant", "content": final.content})
Call get_final_message() inside the with block, after consuming the stream. The with block then closes the connection cleanly.
Pattern 2: Final Message Only (No Display)
You can call get_final_message() without iterating first. It waits until the stream is complete and returns the result:
with client.messages.stream(
model="claude-opus-5-5",
max_tokens=64000,
messages=[{"role": "user", "content": "Write a complete beginner's handbook on Python packaging."}],
) as stream:
message = stream.get_final_message()
Why stream at all if you do not display anything? For long requests, a non-streaming connection stays silent for minutes, and the SDK refuses non-streaming requests that it expects to take longer than about 10 minutes (Lesson 13). Streaming keeps the connection active. According to the documentation, this pattern is especially useful for large max_tokens values, where the SDK requires streaming to avoid HTTP timeouts.
| Request | Recommended approach |
|---|---|
| Short request, no display needed | messages.create() |
Long request (large max_tokens), no display needed | messages.stream() + get_final_message() |
| Any user-facing request | messages.stream() + text_stream + get_final_message() |
get_final_text() vs. get_final_message()
final_text = stream.get_final_text() # str: the answer text only
final_message = stream.get_final_message() # Message: everything
Use get_final_text() when you only need the text, for example to display it again or save it as a document. Use get_final_message() whenever you need anything else, and always for conversation history.
When a Stream Does Not Finish
If a stream is interrupted, for example by a network failure or an error event such as overloaded_error, the SDK raises an exception (Lesson 57), and there is no final message. What you have is whatever arrived before the error:
import anthropic
received = []
try:
with client.messages.stream(model="claude-opus-5-5", max_tokens=4096, messages=messages) as stream:
for text in stream.text_stream:
received.append(text)
final = stream.get_final_message()
except anthropic.APIError as error:
partial_text = "".join(received)
print(f"Stream interrupted after {len(partial_text)} characters: {error}")
The documentation describes a recovery approach for interrupted streams: capture what was received, then make a new request that continues from there. For Claude 4.6 and later models, the continuation goes in a user message, which includes the partial response and asks the model to continue from where it left off, because these models do not support a trailing assistant message (prefill). Only text can be resumed this way. Thinking and tool-use blocks cannot be partially recovered. Lesson 57 implements this.
Practical Example: A Streaming Conversation Turn
This function performs one complete, streamed conversation turn. It displays the answer live, retrieves the final message, applies stop-reason checks, and updates the history only with complete, usable turns.
"""stream_turn.py: a streamed conversation turn that stores complete final messages."""
import json
from pathlib import Path
import anthropic
from anthropic import Anthropic
client = Anthropic()
MODEL = "claude-opus-5-5"
SYSTEM = "You are a friendly Python tutor. Keep answers under 120 words."
def stream_turn(history: list[dict], user_text: str) -> list[dict]:
"""Stream one turn. Return the new history (unchanged if the turn was not usable)."""
pending = history + [{"role": "user", "content": user_text}]
try:
with client.messages.stream(
model=MODEL, max_tokens=2048, system=SYSTEM, messages=pending
) as stream:
print("Claude: ", end="", flush=True)
for text in stream.text_stream:
print(text, end="", flush=True)
final = stream.get_final_message()
except anthropic.APIError as error:
print(f"\n[The response was interrupted: {error}. Please try again.]")
return history
print()
if final.stop_reason == "refusal":
print("[Claude declined this request. It was not saved.]")
return history
if final.stop_reason == "max_tokens":
print("[Note: the answer was cut off.]")
print(f"[{final.usage.output_tokens} output tokens | id {final.id}]")
return pending + [{"role": "assistant", "content": final.to_dict()["content"]}]
if __name__ == "__main__":
history: list[dict] = []
for question in ["What is a Python set?", "How is it different from a list?"]:
print(f"\nYou: {question}")
history = stream_turn(history, question)
Path("chat.json").write_text(json.dumps(history, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"\nSaved {len(history)} messages, including complete assistant content blocks.")
Example output:
You: What is a Python set?
Claude: A set is an unordered collection of unique items, written with curly braces: {1, 2, 3}...
[84 output tokens | id msg_01Q8Faay6S7QPTvEUUQARt7h]
You: How is it different from a list?
Claude: Unlike a list, a set has no order and no duplicates, so you can't index it...
[97 output tokens | id msg_01XFDUDYJgAACzvnptvVoYEL]
Saved 4 messages, including complete assistant content blocks.
How It Works
pendingis a new list with the user message added. The original history is only replaced when the turn succeeds (Lesson 24).- The text is streamed live with
text_stream, so the user sees progress immediately. get_final_message()is called inside thewithblock after the loop and returns the completeMessage.- Errors during streaming (
anthropic.APIError) leave the history unchanged, and the user is asked to try again. No partial turn is stored. - Stop reasons are checked on the final message: refusals are not stored, and truncation is reported (Lesson 20).
final.to_dict()["content"]stores the complete assistant turn as plain data, including any thinking blocks with their signatures, so the history can be saved as JSON and sent back to the API unchanged.
Common Mistakes
Building History from Streamed Text
What happens: thinking blocks and tool calls are lost, and tool-use conversations fail on the next request.
Fix: store get_final_message() content.
Calling get_final_message() After the with Block
What happens: the stream has been closed, so the final message may not be available.
Fix: call it inside the with block.
Using create() for Very Long Outputs
What happens: the SDK raises an error for requests expected to take more than about 10 minutes, or the connection times out.
Fix: use messages.stream() with get_final_message().
Assuming a Final Message Exists After an Error
What happens: code that expects final after an exception raises NameError or uses stale data.
Fix: handle exceptions explicitly, and treat interrupted streams as incomplete.
Ignoring Stop Reasons on Streamed Responses
What happens: truncated or refused streamed answers are stored as normal.
Fix: apply the same stop-reason checks to the final message as to create() responses.
Best Practices
- Always retrieve the final message for any streamed response you store or act on.
- Use streaming plus
get_final_message()for long requests, even without display. - Store complete content blocks in conversation history.
- Check stop reasons and usage on the final message.
- Commit a turn only after the stream completes successfully.
- Treat interrupted streams as incomplete, and recover deliberately (Lesson 57).
Exercise
- Use Pattern 2 to generate a long document with
max_tokens=32000, and print itsstop_reasonandoutput_tokens. - Modify
stream_turn()to also print the time to first text and the total time. - Load
chat.jsonfrom the example, and continue the conversation with a third question usingstream_turn(). - Simulate an interruption by breaking out of the
text_streamloop after 50 characters, then callingget_final_message(). What happens, and why should real interruptions be handled differently?
Step 3:
history = json.loads(Path("chat.json").read_text(encoding="utf-8"))
history = stream_turn(history, "When should I use a set instead of a list?")
Because the stored assistant turns contain their complete content blocks, the history can be sent back exactly as saved.
Step 4: breaking out of the loop does not end the stream by itself. When get_final_message() is called inside the with block, it continues reading the remaining events and returns the complete message. A real interruption is different: the connection fails, an exception is raised, and the rest of the response never arrives. That is why real interruptions need error handling and, if desired, a continuation request (Lesson 57). To deliberately stop a response early, leave the with block instead (Lesson 60).