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