Read Text Blocks

Su Suriya Ravichandran Updated 10 Nov 2026
8 min read ·Lesson 41 of 200

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:

FieldTypeMeaning
typestrAlways "text"
textstrThe text Claude wrote in this block
citationslist or NoneSource 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:

ReasonWhat the blocks look like
CitationsThe answer is split into segments, so each segment can carry its own sources
Tool useA short text before a tool call ("Let me check the weather."), followed by a tool_use block
Server toolsText interleaved with web search calls and their results
Thinking between stepsText, 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 fieldMeaning
typeThe kind of location, such as "char_location" for plain text or "page_location" for PDFs
cited_textThe exact text from the source document that supports this segment
document_indexWhich document in the request (0-based)
document_titleThe document's title, if one was given
start_char_index, end_char_indexCharacter 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

  1. 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).
  2. 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 to footnotes, and a marker such as [1] is inserted directly after the segment it supports.
  3. Joining body_parts with "" reassembles the answer exactly, because the segments are pieces of continuous text.
  4. citation.cited_text is the exact supporting passage, so readers can verify each claim.
  5. Non-text blocks, such as thinking, are skipped by the type check.

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 citations defensively, and show cited text to users when accuracy matters.
  • Use one helper that reads text from SDK objects, dictionaries, and strings.
  • Check stop_reason to understand what the text represents.

Exercise

  1. Ask a question that cannot be answered from POLICY, such as one about Java courses. How many citations appear?
  2. Modify render_with_footnotes() to avoid duplicate footnotes when the same cited_text is cited twice.
  3. Use text_of() to print the last assistant message from a conversation saved with the Conversation class from Lesson 24.
  4. Print the number of text blocks and the number of citations for three different questions. When does Claude split the answer into more blocks?
Show answers for steps 1 and 2

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.

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

Signed-in readers only.