Stream Text Output

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

The Simplest Streaming Pattern

The most common streaming task is showing Claude's answer text as it arrives. The stream helper provides text_stream, an iterator that yields only the answer text, chunk by chunk:

from anthropic import Anthropic

client = Anthropic()

with client.messages.stream(
    model="claude-opus-5-5",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Write a four-line poem about Python."}],
) as stream:
    for text in stream.text_stream:
        print(text, end="", flush=True)

print()

Line by line:

CodePurpose
client.messages.stream(...)Starts a streaming request with the same parameters as create()
with ... as stream:Opens the stream and guarantees it is closed afterward
stream.text_streamYields each new piece of answer text as it arrives
print(text, end="", flush=True)Prints the chunk without a line break, and shows it immediately
print()Ends the line after the stream finishes

What text_stream Contains

IncludedNot included
Text from text blocks, the answerThinking content
Text from every text block, in orderTool call inputs (tool_use blocks)
Event metadata, such as stop reasons and usage

Chunks have no fixed size. A chunk can be a word, part of a word, several words, or just whitespace or a line break. Your code must never assume that a chunk is a complete word, sentence, or line.

Because text_stream contains only answer text, it is the right choice for displaying replies. For thinking, tool calls, or progress events, use the event interface (Lesson 53).


Why end="" and flush=True Matter

print(text, end="", flush=True)
  • end="": print() normally adds a newline after its output. Chunks are fragments of continuous text, so a newline after each chunk would break the text apart.
  • flush=True: Python buffers output, collecting it and writing it in larger batches for efficiency. Without flushing, the text may appear in sudden bursts, or only at the end, which defeats the purpose of streaming.

Collecting the Full Text While Streaming

Often you need both: display the text live and keep the complete text for later use. Collect the chunks in a list, then join them:

chunks = []
with client.messages.stream(
    model="claude-opus-5-5",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Give three tips for writing clean Python."}],
) as stream:
    for text in stream.text_stream:
        print(text, end="", flush=True)
        chunks.append(text)

full_text = "".join(chunks)
print(f"\n\n[{len(full_text)} characters received]")

Why a list instead of full_text += text: appending to a list and joining once is efficient for any length. Repeated string concatenation creates a new string each time, which becomes slow for very long outputs.

Getting the Final Text Directly

The stream helper also accumulates the text for you. After the stream finishes, get_final_text() returns the complete answer text:

with client.messages.stream(model="claude-opus-5-5", max_tokens=1024, messages=messages) as stream:
    for text in stream.text_stream:
        print(text, end="", flush=True)
    full_text = stream.get_final_text()

To also get the stop reason, usage, and all content blocks, use get_final_message() (Lesson 55).


Writing Streamed Text to a File

To save output progressively, for example for long reports, write each chunk as it arrives:

from pathlib import Path

with Path("report.md").open("w", encoding="utf-8") as file, client.messages.stream(
    model="claude-opus-5-5",
    max_tokens=8192,
    messages=[{"role": "user", "content": "Write a detailed guide to Python packaging."}],
) as stream:
    for text in stream.text_stream:
        file.write(text)
        file.flush()
  • A single with statement manages both the file and the stream, so both are closed properly.
  • file.flush() writes each chunk to disk immediately, so the partial output is preserved if the program stops unexpectedly.
  • encoding="utf-8" handles any characters in the output (Lesson 15).

Rendering Streamed Markdown

Claude often formats answers in Markdown. Printing raw chunks shows symbols such as ** and #. The rich library can re-render Markdown live as text arrives:

python -m pip install rich
from rich.live import Live
from rich.markdown import Markdown

buffer = ""
with client.messages.stream(
    model="claude-opus-5-5",
    max_tokens=2048,
    messages=[{"role": "user", "content": "Explain Python decorators with a short code example."}],
) as stream, Live(Markdown(""), refresh_per_second=8) as live:
    for text in stream.text_stream:
        buffer += text
        live.update(Markdown(buffer))

How it works: Live controls a region of the terminal that can be redrawn. Each chunk is added to buffer, and the whole buffer is re-rendered as Markdown. Incomplete Markdown, such as an unclosed code block, renders as plain text until it is completed. refresh_per_second=8 limits how often the screen is redrawn, which keeps the terminal smooth.

Re-rendering the whole buffer each time is fine for typical chat-length answers. For very long documents, redraw less often or print plain text.


Practical Example: A Streaming Terminal Assistant with Statistics

This tool streams an answer with live display and then reports useful statistics: time to first text, total time, output tokens, and the stop reason.

"""stream_cli.py: stream an answer to the terminal and report timing statistics.

Usage:
    python stream_cli.py "Explain Python list comprehensions with examples."
"""

import sys
import time

from anthropic import Anthropic

client = Anthropic()


def stream_answer(question: str) -> None:
    start = time.perf_counter()
    first_text_at = None

    with client.messages.stream(
        model="claude-opus-5-5",
        max_tokens=4096,
        messages=[{"role": "user", "content": question}],
    ) as stream:
        print("Claude: ", end="", flush=True)
        for text in stream.text_stream:
            if first_text_at is None and text.strip():
                first_text_at = time.perf_counter() - start
            print(text, end="", flush=True)
        message = stream.get_final_message()

    total = time.perf_counter() - start
    output_tokens = message.usage.output_tokens

    print("\n")
    print(f"[time to first text: {first_text_at:.1f}s]" if first_text_at else "[no text received]")
    print(f"[total time: {total:.1f}s | output tokens: {output_tokens} "
          f"| ~{output_tokens / total:.0f} tokens/s | stop: {message.stop_reason}]")
    if message.stop_reason == "max_tokens":
        print("[warning: the answer was cut off at max_tokens]")


if __name__ == "__main__":
    question = " ".join(sys.argv[1:]) or "Explain Python list comprehensions with two examples."
    stream_answer(question)

Example output:

Claude: A list comprehension builds a new list in a single expression...
...

[time to first text: 1.8s]
[total time: 7.4s | output tokens: 612 | ~83 tokens/s | stop: end_turn]

How It Works

  1. print("Claude: ", end="", flush=True) shows a label immediately, before any text arrives, so the user knows a response is coming.
  2. first_text_at records when the first non-whitespace chunk arrived, the time to first text from Lesson 51.
  3. stream.get_final_message() is called inside the with block, after the loop. It returns the complete Message, including usage and stop_reason, which the text chunks alone do not provide.
  4. Tokens per second is calculated from output_tokens, which include thinking, divided by the total time. It is an approximate throughput measure, useful for comparing models and settings.
  5. The truncation warning follows the rule from Lesson 20: never present a cut-off answer as complete.
  6. The conditional expression ... if first_text_at else ... handles responses with no text, for example if thinking used the whole max_tokens budget.

Common Mistakes

Printing Each Chunk on Its Own Line

What happens: print(text) adds a newline after every chunk, breaking words and sentences apart.

Fix: use end="".

Forgetting to Flush

What happens: text appears in bursts instead of smoothly.

Fix: use flush=True, or file.flush() when writing to files.

Assuming Chunks Are Words or Lines

What happens: logic such as "capitalize each chunk" or "process each line as it arrives" produces broken results.

Fix: accumulate text, and process complete units, such as lines split from the accumulated buffer.

Expecting Thinking or Tool Calls in text_stream

What happens: tool calls and thinking are silently missing.

Fix: use the event interface (Lesson 53) or the final message (Lesson 55).

Calling get_final_message() After the with Block

What happens: the stream is already closed, and the final message may not be available.

Fix: call it inside the with block, after consuming the text.


Best Practices

  • Use text_stream for displaying answers.
  • Print with end="" and flush=True.
  • Collect chunks in a list and join once, or use get_final_text().
  • Get the final message for stop reasons, usage, and storage.
  • Show a label or indicator immediately, before the first text arrives.
  • Render Markdown live only when the output is meant for human reading in a capable terminal.
  • Warn about truncation after the stream ends.

Exercise

  1. Run stream_cli.py with a short question and a long one. Compare time to first text and tokens per second.
  2. Modify the file-writing example to print a dot (.) to the terminal for every chunk written, as a simple progress indicator.
  3. Process streamed text line by line: accumulate chunks in a buffer, and print each complete line with a line number as soon as it is finished.
  4. Combine rich Markdown rendering with the statistics from stream_cli.py.
Show a possible solution for step 3
buffer = ""
line_number = 0

with client.messages.stream(
    model="claude-opus-5-5",
    max_tokens=2048,
    messages=[{"role": "user", "content": "List 8 Python built-in functions, one per line, with a short description."}],
) as stream:
    for text in stream.text_stream:
        buffer += text
        while "\n" in buffer:
            line, buffer = buffer.split("\n", 1)
            line_number += 1
            print(f"{line_number:>2}: {line}")

if buffer.strip():
    line_number += 1
    print(f"{line_number:>2}: {buffer}")

How it works: chunks are added to buffer. Whenever the buffer contains a line break, the text before it is a complete line, so it is printed and removed from the buffer with split("\n", 1). The while loop handles chunks that contain several line breaks. After the stream ends, any remaining text, the last line without a trailing newline, is printed.

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 Stream Text Output and get answers drawn from it.

Signed-in readers only.