Read Text Blocks
Beyond get_text()
Since Lesson 11, this tutorial has read answers with a one-line helper:
"".join(block.text for block in message.content if block.type == "text")
That helper is correct for most uses. This lesson looks more closely at text blocks themselves: what they contain, why a response can include several of them, when to keep them separate instead of joining them, how to read the citations attached to them, and how to read text from stored conversations, where blocks are plain dictionaries instead of SDK objects.
Anatomy of a Text Block
A text block in a response is a TextBlock object with three fields:
| Field | Type | Meaning |
|---|---|---|
type | str | Always "text" |
text | str | The text Claude wrote in this block |
citations | list or None | Source references for this block's text, when citations are enabled; otherwise None |
for block in message.content:
if block.type == "text":
print(repr(block.text))
print(block.citations)
'Python lists are ordered, mutable collections.'
None
repr() shows the exact string, including any leading or trailing whitespace and newline characters. That is useful when debugging formatting.
Why a Response Can Have Several Text Blocks
A response's content list can contain more than one text block. The main reasons:
| Reason | What the blocks look like |
|---|---|
| Citations | The answer is split into segments, so each segment can carry its own sources |
| Tool use | A short text before a tool call ("Let me check the weather."), followed by a tool_use block |
| Server tools | Text interleaved with web search calls and their results |
| Thinking between steps | Text, thinking, and more text in agent-style responses |
content = [
ThinkingBlock(...),
TextBlock(text="According to the policy, refunds are available "),
TextBlock(text="within 14 days of purchase", citations=[...]),
TextBlock(text=" if less than 30% of the course is completed.", citations=[...]),
]
Joining vs. Keeping Blocks Separate
Join Them for the Plain Answer
When you need the answer as one string, join the text blocks directly, with no separator:
answer = "".join(block.text for block in message.content if block.type == "text")
Why no separator: consecutive text blocks are often pieces of the same sentence, as in the citation example above. Adding "\n" or " " would insert line breaks or extra spaces where none belong.
Keep Them Separate When Blocks Carry Meaning
Process blocks individually when:
- citations must stay attached to the text they support,
- text before a tool call should be shown differently from the final answer, for example as a status message, or
- position matters, for example to render text segments and tool results in order.
segments = [block for block in message.content if block.type == "text"]
for number, segment in enumerate(segments, start=1):
print(f"[{number}] {segment.text!r}")
Reading Citations
When you send documents with citations enabled, Claude attaches source references to the parts of its answer that come from those documents. Lesson 157 covers citations fully. Here is how to read them.
Enabling Citations (Preview)
Citations are enabled per document:
document = {
"type": "document",
"source": {"type": "text", "media_type": "text/plain",
"data": "Refunds are available within 14 days of purchase if less than "
"30% of the course is completed. Certificates are issued within 24 hours."},
"title": "Student Policy",
"citations": {"enabled": True},
}
message = client.messages.create(
model="claude-opus-5-5",
max_tokens=2048,
messages=[{"role": "user", "content": [
document,
{"type": "text", "text": "What is the refund rule?"},
]}],
)
Reading Citation Data
For plain-text documents, each citation is a character location in the source document:
for block in message.content:
if block.type != "text":
continue
print(f"TEXT: {block.text!r}")
for citation in block.citations or []:
print(f" cites {citation.document_title!r}: {citation.cited_text!r}")
TEXT: 'Refunds are available '
TEXT: 'within 14 days of purchase if less than 30% of the course is completed.'
cites 'Student Policy': 'Refunds are available within 14 days of purchase if less than 30% of the course is completed.'
| Citation field | Meaning |
|---|---|
type | The kind of location, such as "char_location" for plain text or "page_location" for PDFs |
cited_text | The exact text from the source document that supports this segment |
document_index | Which document in the request (0-based) |
document_title | The document's title, if one was given |
start_char_index, end_char_index | Character positions in the source (for char_location) |
block.citations or [] handles text blocks that have no citations (None), so the loop works for every block.
Why citations matter: they let users and your code verify an answer against its source, which is essential for policy, legal, medical, and research applications.
Text Blocks in Stored Conversations
When you store a conversation as JSON (Lesson 24), the blocks become plain dictionaries:
{"type": "text", "text": "Python lists are ordered...", "citations": None}
The same content can therefore reach your code in two forms: SDK objects (block.text) from a live response, or dictionaries (block["text"]) from storage. A string is also valid content for a message. A helper that accepts all three forms avoids duplicate code:
def block_field(block, name, default=None):
"""Read a field from an SDK block object or a dict block."""
if isinstance(block, dict):
return block.get(name, default)
return getattr(block, name, default)
def text_of(content) -> str:
"""Return the text of message content: a string, or a list of blocks (objects or dicts)."""
if isinstance(content, str):
return content
return "".join(block_field(b, "text", "") for b in content if block_field(b, "type") == "text")
text_of(message.content) # Live SDK response
text_of(history[-1]["content"]) # Stored assistant turn (dict blocks)
text_of("A plain string message") # String content
Practical Example: An Answer Renderer with Footnotes
This tool asks a question about a document with citations enabled, then renders the answer with numbered footnotes that show the exact supporting text. It reads text blocks individually so that each citation stays attached to its segment.
"""cited_answer.py: render an answer with footnotes from citation data."""
from anthropic import Anthropic
client = Anthropic()
POLICY = (
"Refunds are available within 14 days of purchase if less than 30% of the course is completed. "
"Certificates are issued within 24 hours after the final quiz is passed. "
"Live sessions run every Monday at 7 PM IST, and recordings are posted within 2 hours."
)
def ask_with_citations(question: str):
return client.messages.create(
model="claude-opus-5-5",
max_tokens=2048,
messages=[{"role": "user", "content": [
{
"type": "document",
"source": {"type": "text", "media_type": "text/plain", "data": POLICY},
"title": "Student Policy",
"citations": {"enabled": True},
},
{"type": "text", "text": question},
]}],
)
def render_with_footnotes(message) -> str:
body_parts = []
footnotes = []
for block in message.content:
if block.type != "text":
continue
body_parts.append(block.text)
for citation in block.citations or []:
footnotes.append(citation.cited_text.strip())
body_parts.append(f"[{len(footnotes)}]")
body = "".join(body_parts).strip()
notes = "\n".join(f"[{n}] \"{text}\"" for n, text in enumerate(footnotes, start=1))
return f"{body}\n\nSources:\n{notes}" if footnotes else body
if __name__ == "__main__":
reply = ask_with_citations("When do I get my certificate, and can I watch live sessions later?")
print(render_with_footnotes(reply))
Example output (wording varies):
You'll receive your certificate within 24 hours after passing the final quiz.[1] If you miss a live session, recordings are posted within 2 hours.[2]
Sources:
[1] "Certificates are issued within 24 hours after the final quiz is passed."
[2] "Live sessions run every Monday at 7 PM IST, and recordings are posted within 2 hours."
How It Works
- The document block includes the policy text, a title, and
"citations": {"enabled": True}. It comes before the question, following the "documents first" guidance (Lesson 33). render_with_footnotes()walks the text blocks in order. Each block's text is added to the body. Then, for each of its citations, the cited text is added tofootnotes, and a marker such as[1]is inserted directly after the segment it supports.- Joining
body_partswith""reassembles the answer exactly, because the segments are pieces of continuous text. citation.cited_textis the exact supporting passage, so readers can verify each claim.- Non-text blocks, such as thinking, are skipped by the
typecheck.
Common Mistakes
Taking Only the First Text Block
What happens: with citations or tool use, most of the answer is lost.
Fix: collect all text blocks.
Joining with Separators
What happens: "\n".join(...) breaks sentences that were split across citation segments.
Fix: join with "", or process blocks individually.
Assuming citations Is Always a List
What happens: for c in block.citations raises TypeError when citations is None.
Fix: use block.citations or [].
Mixing Object and Dictionary Access
What happens: block.text fails on stored dict blocks, and block["text"] fails on SDK objects.
Fix: use a helper that accepts both forms.
Showing Pre-Tool Text as the Final Answer
What happens: "Let me check that for you." is displayed as if it were the answer.
Fix: check stop_reason. When it is tool_use, the text is a preamble and the real answer comes after the tool loop (Lesson 88).
Best Practices
- Join all text blocks with
""when you need the answer as one string. - Keep blocks separate when citations, tool calls, or ordering matter.
- Handle
citationsdefensively, and show cited text to users when accuracy matters. - Use one helper that reads text from SDK objects, dictionaries, and strings.
- Check
stop_reasonto understand what the text represents.
Exercise
- Ask a question that cannot be answered from
POLICY, such as one about Java courses. How many citations appear? - Modify
render_with_footnotes()to avoid duplicate footnotes when the samecited_textis cited twice. - Use
text_of()to print the last assistant message from a conversation saved with theConversationclass from Lesson 24. - Print the number of text blocks and the number of citations for three different questions. When does Claude split the answer into more blocks?
Step 1: Claude typically says the document does not mention it, and there are no citations, because no source text supports the answer.
Step 2: reuse the existing footnote number when the same text appears again:
def render_with_footnotes(message) -> str:
body_parts, footnotes = [], []
for block in message.content:
if block.type != "text":
continue
body_parts.append(block.text)
for citation in block.citations or []:
text = citation.cited_text.strip()
if text not in footnotes:
footnotes.append(text)
body_parts.append(f"[{footnotes.index(text) + 1}]")
body = "".join(body_parts).strip()
notes = "\n".join(f"[{n}] \"{t}\"" for n, t in enumerate(footnotes, start=1))
return f"{body}\n\nSources:\n{notes}" if footnotes else body
footnotes.index(text) + 1 finds the existing footnote's number, so repeated citations share one footnote.