Print Response Text
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:
- Extract the text from the right content blocks.
- Check that the text is complete and present.
- Clean it, for example by trimming whitespace.
- 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.
| Situation | stop_reason | What get_text() returns |
|---|---|---|
| Normal answer | end_turn | The full answer |
| Output limit reached | max_tokens | A cut-off answer, or "" if thinking used the whole budget |
| Claude declined | refusal | Possibly little or no text |
| Tool call requested | tool_use | Possibly 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().columnsreads 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.
| Destination | Recommended approach |
|---|---|
| Terminal for developers | Render with rich |
| Plain-text channel (SMS, logs, plain email) | Ask for plain text in the prompt |
| Web page | Convert Markdown to HTML and sanitize it |
| File for later reading | Save 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:
- 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.
- 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_streamyields pieces of text as they are generated. It contains only answer text, not thinking.end=""prevents a line break after each piece.flush=Truemakes 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"importsrichonly when needed. Users who never choose this style do not needrichinstalled.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
- Print a response three ways:
print(message),print(message.content), andprint(get_text(message)). Note the differences. - Ask Claude for a Markdown table comparing three Python web frameworks. Print it with
style="plain"and then withstyle="markdown". - Modify
display_text()so that truncated answers end with"... [truncated: raise max_tokens]"instead of the current notice. - Save an answer that contains non-English text, such as a sentence in Tamil and in Japanese, to
answer.txtwith UTF-8 encoding, and open the file to confirm the characters are correct.
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.