Print Response Text

Su Suriya Ravichandran Updated 10 Nov 2026
9 min read ·Lesson 15 of 200

The Goal: Show Only the Answer

Lesson 14 showed that a response contains much more than the answer: IDs, content blocks, stop reasons, and usage data. Most of the time, users only need Claude's text, displayed cleanly.

Printing it correctly involves four steps:

  1. Extract the text from the right content blocks.
  2. Check that the text is complete and present.
  3. Clean it, for example by trimming whitespace.
  4. Display it in a way that suits where it is shown: a terminal, a file, or a web page.

This lesson covers each step.


What Not to Print

Beginners often try these first:

print(message)
Message(id='msg_01XF...', content=[ThinkingBlock(...), TextBlock(text='Django, Flask, and FastAPI...', type='text', ...)], model='claude-opus-5-5', ...)
print(message.content)
[ThinkingBlock(signature='EuYB...', thinking='', type='thinking'), TextBlock(citations=None, text='Django, Flask, and FastAPI...', type='text')]

Both print the Python representation of objects, including internal structure. That is useful for debugging but not for users.


Step 1: Extract the Text

The correct way is to collect the text of every block whose type is "text":

from anthropic.types import Message


def get_text(message: Message) -> str:
    """Return the combined text of all text blocks in a response."""
    return "".join(block.text for block in message.content if block.type == "text")
print(get_text(message))
Django, Flask, and FastAPI are three popular Python web frameworks.

Why "".join with an empty separator: a single answer is sometimes split across several consecutive text blocks. This happens, for example, when citations are attached to parts of the text (Lesson 157). The blocks are pieces of one continuous answer, so they are joined directly, without adding spaces or line breaks that were not in the original.

Getting the First Text Block Only

Occasionally you only want the first piece of text. Use next() with a default:

first_text = next((block.text for block in message.content if block.type == "text"), "")

next() returns the first value produced by the generator. The second argument, "", is returned if there are no text blocks, which avoids a StopIteration error.


Step 2: Check That the Text Is Usable

The text can be incomplete or missing. Printing it without checking can mislead users.

Situationstop_reasonWhat get_text() returns
Normal answerend_turnThe full answer
Output limit reachedmax_tokensA cut-off answer, or "" if thinking used the whole budget
Claude declinedrefusalPossibly little or no text
Tool call requestedtool_usePossibly a short preamble; the real work is pending

A display function should handle these cases explicitly:

def display_text(message: Message) -> str:
    text = get_text(message).strip()

    if message.stop_reason == "refusal":
        return "[Claude declined this request.]"
    if not text:
        return f"[No text in the response (stop_reason={message.stop_reason}).]"
    if message.stop_reason in {"max_tokens", "model_context_window_exceeded"}:
        return text + "\n\n[Response truncated.]"
    return text

Why this order: the refusal check comes first, because a refusal should never be presented as an answer. The empty-text check prevents printing a blank line with no explanation. The truncation check appends a visible notice, so users know the answer is incomplete.


Step 3: Clean the Text

Trim Whitespace

Answers can begin or end with blank lines or spaces. .strip() removes them:

print(get_text(message).strip())

.strip() affects only the beginning and end. Line breaks and indentation inside the answer, which matter for lists and code, are preserved.

Wrap Long Lines in Terminals

Long paragraphs can be hard to read in a terminal. textwrap wraps them to a chosen width:

import shutil
import textwrap

width = min(shutil.get_terminal_size().columns, 100)

for paragraph in get_text(message).strip().split("\n"):
    print(textwrap.fill(paragraph, width=width) if paragraph else "")
  • shutil.get_terminal_size().columns reads the current terminal width. min(..., 100) caps it for readability on very wide screens.
  • The text is processed line by line, so existing line breaks and blank lines between paragraphs are kept.
  • Empty lines are printed as they are, since wrapping an empty string would remove the paragraph break.

Warning: Do not wrap code. Re-wrapping changes indentation and line breaks, which breaks code and some Markdown structures such as tables. Use wrapping for prose only, or render Markdown properly, as shown next.


Step 4: Display Markdown Properly

Claude often formats answers with Markdown: **bold**, # headings, bullet lists, and fenced code blocks. In a plain terminal, those symbols appear literally:

## Key differences

- **Django** includes an ORM and admin panel.
- **Flask** is minimal and flexible.

You have two good options.

Option A: Ask for Plain Text

If the output goes somewhere that does not render Markdown, such as SMS, plain-text email, or a log, tell Claude in the prompt:

message = client.messages.create(
    model="claude-opus-5-5",
    max_tokens=1024,
    messages=[{
        "role": "user",
        "content": "Compare Django and Flask in three sentences. "
                   "Use plain text only, with no Markdown formatting.",
    }],
)

This solves the problem at the source. Lesson 17 shows how to set formatting rules once, in a system prompt.

Option B: Render Markdown in the Terminal

The popular rich library renders Markdown with colors, bold text, and highlighted code:

python -m pip install rich
from rich.console import Console
from rich.markdown import Markdown

console = Console()
console.print(Markdown(get_text(message)))
  • Markdown(...) parses the text as Markdown.
  • console.print(...) renders it with terminal styling: headings, bold, lists, and syntax-highlighted code blocks.
DestinationRecommended approach
Terminal for developersRender with rich
Plain-text channel (SMS, logs, plain email)Ask for plain text in the prompt
Web pageConvert Markdown to HTML and sanitize it
File for later readingSave as .md

Printing to Files

To save an answer, write it as UTF-8 text:

from pathlib import Path

Path("answer.md").write_text(get_text(message), encoding="utf-8")

Why specify encoding="utf-8": Claude's answers can contain characters outside basic ASCII, such as accented letters, non-Latin scripts, emoji, and typographic quotes. Without an explicit encoding, Python uses the operating system's default, which on some Windows systems cannot represent every character. The write then fails or produces garbled text.

Unicode Errors When Printing on Windows

When output is redirected to a file or pipe on Windows (python ask.py > out.txt), Python may use a legacy encoding and raise:

UnicodeEncodeError: 'charmap' codec can't encode character ...

Fix it in one of these ways:

import sys

sys.stdout.reconfigure(encoding="utf-8")   # At the start of the program
python -X utf8 ask.py > out.txt            # Enable Python's UTF-8 mode for one run

Setting the environment variable PYTHONUTF8=1 enables UTF-8 mode permanently.


Printing in Web Applications

In a web application, you return the text instead of printing it. Two rules matter:

  1. Never insert Claude's output into HTML without escaping or sanitizing it. The text could contain HTML or script tags, especially if the prompt included content from users or the web. Unescaped output is a cross-site scripting (XSS) risk.
  2. If you render Markdown to HTML, sanitize the resulting HTML with an allowlist-based sanitizer before sending it to the browser.

In a Django template, normal variable output is escaped automatically:

<!-- Safe: Django escapes the text, and linebreaksbr keeps line breaks -->
<p>{{ answer|linebreaksbr }}</p>

Avoid marking model output as safe (|safe) unless it has been sanitized.


Showing Text as It Is Generated

print(get_text(message)) shows the answer only after it has been fully generated. For long answers, users wait with nothing on screen. Streaming (Lessons 51–60) prints text as it arrives:

with client.messages.stream(
    model="claude-opus-5-5",
    max_tokens=2048,
    messages=[{"role": "user", "content": "Explain Python decorators."}],
) as stream:
    for text in stream.text_stream:
        print(text, end="", flush=True)
print()
  • stream.text_stream yields pieces of text as they are generated. It contains only answer text, not thinking.
  • end="" prevents a line break after each piece.
  • flush=True makes each piece appear immediately instead of waiting in an output buffer.

This is previewed here because it is the most common way to "print response text" in interactive tools.


Practical Example: A Reusable Answer Printer

This module combines extraction, checking, cleaning, and display into one function with selectable output styles:

"""printer.py: display Claude's answer in plain, wrapped, or Markdown form."""

import shutil
import sys
import textwrap

from anthropic import Anthropic
from anthropic.types import Message


def get_text(message: Message) -> str:
    return "".join(block.text for block in message.content if block.type == "text")


def display_text(message: Message) -> str:
    text = get_text(message).strip()
    if message.stop_reason == "refusal":
        return "[Claude declined this request.]"
    if not text:
        return f"[No text in the response (stop_reason={message.stop_reason}).]"
    if message.stop_reason in {"max_tokens", "model_context_window_exceeded"}:
        return text + "\n\n[Response truncated.]"
    return text


def print_answer(message: Message, style: str = "plain") -> None:
    text = display_text(message)

    if style == "markdown":
        from rich.console import Console
        from rich.markdown import Markdown

        Console().print(Markdown(text))
    elif style == "wrapped":
        width = min(shutil.get_terminal_size().columns, 100)
        for line in text.split("\n"):
            print(textwrap.fill(line, width=width) if line else "")
    else:
        print(text)


if __name__ == "__main__":
    style = sys.argv[1] if len(sys.argv) > 1 else "plain"

    client = Anthropic()
    reply = client.messages.create(
        model="claude-opus-5-5",
        max_tokens=2048,
        messages=[{
            "role": "user",
            "content": "Give a short Markdown guide to Python list comprehensions "
                       "with one code example.",
        }],
    )
    print_answer(reply, style=style)

Try each style:

python printer.py plain
python printer.py wrapped
python printer.py markdown

How It Works

  • display_text() handles refusals, missing text, and truncation once, so every style benefits from the same safety checks.
  • style="plain" prints the text exactly as returned. Use this when piping output to other programs or files.
  • style="wrapped" wraps prose to the terminal width while preserving existing line breaks.
  • style="markdown" imports rich only when needed. Users who never choose this style do not need rich installed.
  • sys.argv[1] reads the style from the command line, defaulting to "plain".

Common Mistakes

Printing the Object Instead of the Text

What happens: users see Message(...) or [TextBlock(...)].

Fix: print get_text(message).

Printing Truncated or Missing Answers Silently

What happens: half an answer, or an empty line, appears with no explanation.

Fix: check stop_reason and empty text before printing, as display_text() does.

Joining Text Blocks with Extra Separators

What happens: "\n".join(...) inserts line breaks into the middle of sentences when an answer is split across several text blocks.

Fix: use "".join(...).

Wrapping Code with textwrap

What happens: code indentation and line breaks are destroyed.

Fix: wrap prose only, or render Markdown with rich.

Inserting Model Output into HTML Unescaped

What happens: a potential XSS vulnerability.

Fix: rely on template auto-escaping, and sanitize any HTML generated from Markdown.

Writing Files Without an Encoding

What happens: UnicodeEncodeError or garbled characters on some systems.

Fix: always pass encoding="utf-8".


Best Practices

  • Use one helper for extracting and checking text, and reuse it everywhere.
  • Handle refusals, truncation, and empty responses with clear messages.
  • Choose formatting by destination: request plain text where Markdown is not rendered, render Markdown in rich terminals, and sanitize HTML on the web.
  • Always use UTF-8 for files and redirected output.
  • Stream long answers in interactive tools, so users see progress immediately.

Exercise

  1. Print a response three ways: print(message), print(message.content), and print(get_text(message)). Note the differences.
  2. Ask Claude for a Markdown table comparing three Python web frameworks. Print it with style="plain" and then with style="markdown".
  3. Modify display_text() so that truncated answers end with "... [truncated: raise max_tokens]" instead of the current notice.
  4. Save an answer that contains non-English text, such as a sentence in Tamil and in Japanese, to answer.txt with UTF-8 encoding, and open the file to confirm the characters are correct.
Show answers for steps 3 and 4

Step 3:

if message.stop_reason in {"max_tokens", "model_context_window_exceeded"}:
    return text + " ... [truncated: raise max_tokens]"

Step 4:

from pathlib import Path

reply = client.messages.create(
    model="claude-opus-5-5",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Say 'Welcome' in Tamil and in Japanese."}],
)
Path("answer.txt").write_text(get_text(reply), encoding="utf-8")
print(Path("answer.txt").read_text(encoding="utf-8"))

Reading the file back with the same encoding confirms that the characters were stored correctly.

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

Signed-in readers only.