Streaming a Response

Consume a reply chunk by chunk, assemble the full text as you go, and detect a stream that ends before the model finished.

Syntaxwith client.messages.stream(model=..., max_tokens=..., messages=[...]) as stream: for text in stream.text_stream: ...

A non-streamed call returns nothing until the model has finished generating, which for a long answer is tens of seconds of blank screen. Streaming sends the same text in small deltas as it is produced. Total time barely changes; time to first visible character drops to a fraction of a second, and that is the whole point.

The two forms

The SDK helper is client.messages.stream(...) used as a context manager. Iterating stream.text_stream yields plain text deltas, and stream.get_final_message() returns the assembled message afterwards, with usage and stop_reason intact. Under it is the longhand: client.messages.create(..., stream=True) yields raw events (content_block_delta, message_stop) and accumulates nothing for you. The helper is the same stream with the bookkeeping done; drop to the longhand only when you need event types the helper hides.

Assembling and finishing

Append every delta to a list and "".join at the end rather than using += in a loop. Flush as you print, because buffered stdout defeats the exercise. Critically, a stream is finished only when its terminal event arrives: if the connection drops you get a short reply and no error. Record whether you saw the end, and treat a partial reply as a failure rather than as an answer.

What trips people up

Streamed text cannot be validated before the user sees it. If the reply must be JSON or must pass a safety check, either do not stream it, or stream into a buffer and render only after the check passes.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Making a chat reply feel immediate instead of frozen for twenty seconds
  • Forwarding model output to a browser over server-sent events
  • Showing progress for a long generation in a command-line tool
  • Detecting and retrying a reply whose connection dropped halfway

More examples

The real stream, with the final message

The shortest correct streaming loop. text_stream hides the event types and yields only text; get_final_message gives back the same message a non-streaming call would have returned, so usage accounting and stop_reason checks are unchanged. Note the explicit flush: without it the terminal buffers and the user sees one burst at the end.

Example · python
import anthropic

client = anthropic.Anthropic()

with client.messages.stream(
    model="claude-opus-5-5",
    max_tokens=4096,
    messages=[{"role": "user", "content": "Explain TCP backpressure."}],
) as stream:
    for text in stream.text_stream:
        print(text, end="", flush=True)   # flush, or nothing appears until the end
    final = stream.get_final_message()

print()
print(final.stop_reason, final.usage.output_tokens)
# Exiting the with block closes the HTTP response. Do not leak the iterator
# out of the block and expect to keep reading from it.

Raw events when you need more than text

Iterating the stream itself rather than text_stream, which is what you need when a reply contains more than one kind of block and the UI should label them. This is the same data text_stream filters down to; reach for it only when the extra event types are doing work for you.

Example · python
with client.messages.stream(
    model="claude-opus-5-5", max_tokens=4096, messages=messages,
) as stream:
    for event in stream:
        if event.type == "content_block_start":
            print(f"\n[{event.content_block.type}]")
        elif event.type == "content_block_delta":
            if event.delta.type == "text_delta":
                print(event.delta.text, end="", flush=True)

Streaming out of a web handler

Forwarding a model stream to a browser as SSE. The subtle part is lifetime: if you open the stream, return a generator, and let the with block exit, the HTTP response is already closed by the time the server iterates. Keeping the context manager inside the generator body is what makes it work. The explicit done and truncated events let the client tell a finished answer from a cut-off one.

Example · python
def sse_tokens(prompt):
    """Server-sent events: one data: line per delta, blank line terminates."""
    with client.messages.stream(
        model="claude-opus-5-5", max_tokens=4096,
        messages=[{"role": "user", "content": prompt}],
    ) as stream:
        for text in stream.text_stream:
            yield f"data: {text}\n\n".encode()
        if stream.get_final_message().stop_reason == "max_tokens":
            yield b"event: truncated\ndata: \n\n"
    yield b"event: done\ndata: \n\n"

# The generator must stay alive while the response is being written, so the
# with block has to live inside it, not around the call that returns it.

Discussion

  • Be the first to comment on this lesson.