Get Final Messages

Su Suriya Ravichandran Updated 10 Nov 2026
7 min read ·Lesson 55 of 200

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:

InformationIn text_stream chunks?In the final message?
Answer textYes, in piecesYes, in text blocks
Thinking blocks (with signatures)NoYes
Tool calls with complete inputsNoYes
CitationsNoYes
stop_reason and stop_sequenceNoYes
usage (final, cumulative)NoYes
Message IDNoYes

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.

RequestRecommended approach
Short request, no display neededmessages.create()
Long request (large max_tokens), no display neededmessages.stream() + get_final_message()
Any user-facing requestmessages.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

  1. pending is a new list with the user message added. The original history is only replaced when the turn succeeds (Lesson 24).
  2. The text is streamed live with text_stream, so the user sees progress immediately.
  3. get_final_message() is called inside the with block after the loop and returns the complete Message.
  4. Errors during streaming (anthropic.APIError) leave the history unchanged, and the user is asked to try again. No partial turn is stored.
  5. Stop reasons are checked on the final message: refusals are not stored, and truncation is reported (Lesson 20).
  6. 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

  1. Use Pattern 2 to generate a long document with max_tokens=32000, and print its stop_reason and output_tokens.
  2. Modify stream_turn() to also print the time to first text and the total time.
  3. Load chat.json from the example, and continue the conversation with a third question using stream_turn().
  4. Simulate an interruption by breaking out of the text_stream loop after 50 characters, then calling get_final_message(). What happens, and why should real interruptions be handled differently?
Show answers for steps 3 and 4

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).

0 Comments

Reviewed before they appear

No comments yet.

Anthropic SDK Using Python
Anthropic SDK Intro Claude API Intro SDK Requirements Install Anthropic SDK Upgrade the SDK Create an API Key Set API Keys Use Environment Variables Import Anthropic Create a Client Send Your First Message Choose a Claude Model Set Max Tokens Read a Response Print Response Text Understand Message Blocks Use System Prompts Send User Messages Set Temperature Handle Stop Reasons Messages API Intro Create Messages Use Message Roles Send Conversation History Continue a Conversation Build a Chat Loop Reset Chat History Use Multiple Turns Inspect Message IDs Read Usage Data Write Clear Prompts Set a System Prompt Add Context to Prompts Use Prompt Templates Add Examples to Prompts Request JSON Output Use XML Tags Control Response Length Improve Prompt Quality Avoid Prompt Injection Read Text Blocks Read Content Blocks Check Stop Reasons Count Input Tokens Count Output Tokens Count Tokens Before Sending Limit Output Tokens Use Stop Sequences Read Request IDs Parse API Responses Streaming Intro Stream Text Output Use Stream Events Use Stream Helpers Get Final Messages Stream Async Responses Handle Stream Errors Build a Live Chat Stream Long Responses Close Streams Safely Build a Chatbot Keep Chat History Manage Chat Sessions Add Chat Memory Build a CLI Chatbot Build a Web Chatbot Use Flask with Claude Use FastAPI with Claude Return JSON from an API Add Chat Error Messages Async SDK Intro Create an Async Client Send Async Messages Use asyncio Run Parallel Requests Stream Async Text Handle Async Errors Set Async Timeouts Close Async Clients Choose Sync or Async Tool Use Intro Define a Tool Create Tool Schemas Send Tools to Claude Read Tool Calls Run Python Tools Return Tool Results Continue After Tool Use Control Tool Choice Handle Tool Errors Use the beta_tool Decorator Create a Tool Runner Add Tool Docstrings Validate Tool Inputs Use Multiple Tools Limit Tool Calls Log Tool Activity Build a Weather Tool Build a Search Tool Build a Calculator Tool Structured Output Intro Request JSON Data Validate JSON Output Use Pydantic Models Handle Invalid JSON Extract Fields from Text Generate Typed Data Use Image URLs Send Base64 Images Read PDFs with Claude Use Output Schemas Build Data Extraction Test Structured Responses Send Image Inputs Upload Files Use File IDs Process Text Files Summarize Documents Extract Table Data Handle File Errors Read Cache Usage Reduce Repeated Input Costs Debug Cache Misses Prompt Caching Intro Enable Prompt Caching Cache System Prompts Cache Long Context Test Cache Performance Use Cache Breakpoints Cache Best Practices Message Batches Intro Create a Message Batch Set Batch IDs Check Batch Status List Message Batches Get Batch Results Process Batch Outputs Handle Failed Batch Items Use Batches for Bulk Tasks Compare Batches and Requests Handle API Errors Handle Rate Limits Retry Failed Requests Set Request Timeouts Handle Network Errors Use Exponential Backoff Build Reliable API Calls Use Extended Thinking Log API Errors Track Request IDs Paginate API Results Set Thinking Budgets Read Thinking Blocks Use Tool Search Use Web Search Tools Use Computer Tools Use Citations Use Token Counting Read Raw Responses Set Custom Headers Use Claude on AWS Bedrock Use Claude on Vertex AI Use Claude on Microsoft Foundry Configure Cloud Credentials Choose a Cloud Provider Compare API Integrations Configure Provider Clients Handle Cloud Errors Use Cloud SDK Extras Deploy Cloud Apps Protect API Keys Use a .env File Ignore Secrets in Git Validate User Input Reduce Prompt Injection Limit Tool Permissions Avoid Unsafe Tool Calls Sanitize Tool Results Set Usage Limits Review Security Logs Test SDK Functions Mock API Responses Test Error Handling Test Streaming Code Measure Token Usage Track API Costs Configure Logging Deploy with Docker Deploy a FastAPI App Monitor Production Apps Build a Claude Chatbot Build a Text Summarizer Build a Code Explainer Build a JSON Extractor Build a PDF Assistant Build a Streaming Chat App Build a Tool-Calling Agent Build a Batch Processor Build a Research Assistant Build a Claude API Dashboard
Ask about this post
AI Ask about this post

Ask questions about Get Final Messages and get answers drawn from it.

Signed-in readers only.