Read Content Blocks

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

Every Response Is a List of Blocks

Lesson 16 introduced content blocks, and Lesson 41 focused on text blocks. Real applications, especially those that use tools, thinking, or server-side features, must handle all block types in a response reliably.

This lesson builds the tools for that:

  • a complete overview of response block types and their fields,
  • a registry pattern that routes each block to a handler,
  • a structured response view that groups blocks by purpose, and
  • techniques for handling unknown block types and testing without API calls.

Response Block Types and Their Fields

typeKey fieldsWhat your code typically does
texttext, citationsShow to the user; read citations (Lesson 41)
thinkingthinking, signatureUsually hide; keep unchanged in history (Lessons 16, 153)
redacted_thinkingdataHide; keep unchanged in history
tool_useid, name, inputRun the named tool with input, and return a result (Lessons 85–88)
server_tool_useid, name, inputInformational: a server-side tool, such as web search, ran on Anthropic's side
Server tool results (e.g. web_search_tool_result)tool_use_id, contentInformational: the results of a server-side tool (Lesson 155)

Three facts shape how code should treat blocks:

  1. Order is meaningful. Blocks appear in the order they were produced, for example thinking → text → tool call.
  2. Several blocks of the same type can appear, such as multiple text segments or multiple tool calls.
  3. New block types can be introduced as the API adds features. Robust code handles unknown types without crashing.

Approach 1: match Statements

For a few block types, a match statement (Lesson 16) is clear and sufficient:

for block in message.content:
    match block.type:
        case "text":
            print(block.text)
        case "tool_use":
            print(f"Tool call: {block.name}({block.input})")
        case "thinking" | "redacted_thinking":
            pass
        case _:
            print(f"[unhandled block type: {block.type}]")

As an application grows, with more block types, more behaviors per type, and different behavior in different contexts, a single large match becomes harder to maintain.


Approach 2: A Handler Registry

A registry maps each block type to a handler function. Adding support for a new type means adding one entry, without touching the dispatch logic:

from collections.abc import Callable

Handler = Callable[[object], None]


def handle_text(block) -> None:
    print(block.text)


def handle_tool_use(block) -> None:
    print(f"Tool call: {block.name}({block.input})")


def ignore(block) -> None:
    pass


HANDLERS: dict[str, Handler] = {
    "text": handle_text,
    "tool_use": handle_tool_use,
    "thinking": ignore,
    "redacted_thinking": ignore,
}


def dispatch(content) -> None:
    for block in content:
        handler = HANDLERS.get(block.type)
        if handler is None:
            print(f"[unhandled block type: {block.type}]")
            continue
        handler(block)

How it works:

  • HANDLERS is a dictionary from type names to functions.
  • HANDLERS.get(block.type) returns the handler, or None for unknown types.
  • Unknown types are reported and skipped, so the program continues safely.
  • Different parts of an application can use different registries, for example one that displays thinking in a debug view and one that hides it in production.

Approach 3: A Structured Response View

Often you do not want to do something with each block immediately. You want to organize the response so the rest of your code can ask simple questions: "What is the text?", "Which tools were requested?", "Did anything unexpected appear?"

from dataclasses import dataclass, field


@dataclass
class ToolCall:
    id: str
    name: str
    input: dict


@dataclass
class ResponseView:
    text: str = ""
    tool_calls: list[ToolCall] = field(default_factory=list)
    server_tools_used: list[str] = field(default_factory=list)
    thinking_blocks: int = 0
    unknown_types: list[str] = field(default_factory=list)

The rest of the application then works with view.text and view.tool_calls, without iterating over raw blocks again.


Handling Objects and Dictionaries

Content arrives as SDK objects in live responses and as dictionaries when loaded from storage (Lesson 41). A field-reading helper lets the same code handle both, which also makes testing much easier, because test data can be written as plain dictionaries:

def field_of(block, name, default=None):
    if isinstance(block, dict):
        return block.get(name, default)
    return getattr(block, name, default)

Practical Example: A Content Block Reader

This module builds a ResponseView from any content list, whether objects or dictionaries. It demonstrates the reader with an offline test, using dictionaries and no API call, and with a live request that includes a tool, so a tool_use block appears.

"""block_reader.py: organize any response content into a structured view."""

from dataclasses import dataclass, field

from anthropic import Anthropic


def field_of(block, name, default=None):
    if isinstance(block, dict):
        return block.get(name, default)
    return getattr(block, name, default)


@dataclass
class ToolCall:
    id: str
    name: str
    input: dict


@dataclass
class ResponseView:
    text: str = ""
    tool_calls: list[ToolCall] = field(default_factory=list)
    server_tools_used: list[str] = field(default_factory=list)
    thinking_blocks: int = 0
    unknown_types: list[str] = field(default_factory=list)

    @property
    def needs_tool_results(self) -> bool:
        return bool(self.tool_calls)


def read_content(content) -> ResponseView:
    view = ResponseView()
    text_parts = []

    for block in content:
        block_type = field_of(block, "type")
        if block_type == "text":
            text_parts.append(field_of(block, "text", ""))
        elif block_type == "tool_use":
            view.tool_calls.append(ToolCall(
                id=field_of(block, "id"),
                name=field_of(block, "name"),
                input=dict(field_of(block, "input", {}) or {}),
            ))
        elif block_type == "server_tool_use":
            view.server_tools_used.append(field_of(block, "name"))
        elif block_type in ("thinking", "redacted_thinking"):
            view.thinking_blocks += 1
        elif block_type and block_type.endswith("_tool_result"):
            pass  # Results of server-side tools; informational here
        else:
            view.unknown_types.append(str(block_type))

    view.text = "".join(text_parts)
    return view


def offline_test() -> None:
    content = [
        {"type": "thinking", "thinking": "", "signature": "sig"},
        {"type": "text", "text": "Let me check the weather."},
        {"type": "tool_use", "id": "toolu_01", "name": "get_weather", "input": {"city": "Chennai"}},
        {"type": "future_block_type", "data": "..."},
    ]
    view = read_content(content)
    assert view.text == "Let me check the weather."
    assert view.tool_calls[0].name == "get_weather"
    assert view.tool_calls[0].input == {"city": "Chennai"}
    assert view.thinking_blocks == 1
    assert view.unknown_types == ["future_block_type"]
    print("Offline test passed:", view)


WEATHER_TOOL = {
    "name": "get_weather",
    "description": "Get the current weather for a city.",
    "input_schema": {
        "type": "object",
        "properties": {"city": {"type": "string", "description": "City name"}},
        "required": ["city"],
    },
}


def live_demo() -> None:
    message = Anthropic().messages.create(
        model="claude-opus-5-5",
        max_tokens=2048,
        tools=[WEATHER_TOOL],
        messages=[{"role": "user", "content": "What's the weather in Chennai right now?"}],
    )
    view = read_content(message.content)
    print(f"stop_reason: {message.stop_reason}")
    print(f"text: {view.text!r}")
    for call in view.tool_calls:
        print(f"tool call: {call.name}({call.input}) id={call.id}")
    print(f"needs tool results: {view.needs_tool_results}")


if __name__ == "__main__":
    offline_test()
    live_demo()

Example output (the live part varies):

Offline test passed: ResponseView(text='Let me check the weather.', tool_calls=[ToolCall(id='toolu_01', name='get_weather', input={'city': 'Chennai'})], server_tools_used=[], thinking_blocks=1, unknown_types=['future_block_type'])
stop_reason: tool_use
text: ''
tool call: get_weather({'city': 'Chennai'}) id=toolu_01D7FLrfh4GYq7yT1ULFeyMV
needs tool results: True

How It Works

  1. field_of() reads fields from either SDK objects or dictionaries, so read_content() works with live responses and stored content alike.
  2. read_content() walks the blocks in order and sorts each one into the view: text is collected and joined, tool calls become ToolCall objects, server tool use is recorded, and thinking is counted.
  3. dict(... or {}) makes a plain copy of the tool input, and treats a missing input as an empty dictionary.
  4. endswith("_tool_result") recognizes the family of server-tool result blocks, such as web_search_tool_result, without listing each one.
  5. Unknown types are recorded, not ignored. The offline test includes an invented future_block_type to prove that the reader survives block types it does not recognize, and reports them. In production, log unknown_types so you notice when your code needs updating.
  6. needs_tool_results gives the rest of the application one simple question to ask. If it is true, run the tools and continue the conversation (Lessons 85–88).
  7. The offline test uses plain dictionaries and assert statements, so the reader logic can be tested quickly and for free, without network access. Lessons 181–182 extend this approach.

In the live demo, the text is empty: Claude went straight to the tool call. Depending on the request, Claude may also write a short preamble before the call. Code must handle both.


Common Mistakes

Handling Only Text Blocks

What happens: tool calls are silently ignored, and the application shows an empty or incomplete answer.

Fix: check for tool_use blocks and stop_reason == "tool_use", and handle them.

Crashing on Unknown Types

What happens: a new API feature introduces a block type, and code without a default branch raises an exception or drops content.

Fix: always include a fallback that logs unknown types.

Losing Block Order

What happens: collecting blocks into separate lists too early loses the information that a text segment came before a tool call.

Fix: keep the original content list (for history), and build views from it as needed.

Modifying Blocks Before Sending Them Back

What happens: in tool-use conversations, edited or reordered thinking blocks cause 400 errors.

Fix: store and resend the original content unchanged (Lesson 24). Use views only for reading.

Testing Only with Live API Calls

What happens: slow, costly tests that are hard to reproduce.

Fix: test block-handling logic with dictionary fixtures, as in offline_test().


Best Practices

  • Handle every block type deliberately: show, run, hide, or log.
  • Use a registry or a structured view once more than a few block types matter.
  • Preserve block order and the original content for conversation history.
  • Log unknown block types to detect API changes early.
  • Support both objects and dictionaries when content may come from storage.
  • Test block handling offline with dictionary fixtures.

Exercise

  1. Add a handler for server_tool_use blocks to the registry in Approach 2 that prints "Searching the web…" when the tool's name is web_search.
  2. Extend ResponseView with a citations_count field, and fill it in read_content() from text blocks' citations.
  3. Write a second offline test whose content contains two tool calls, and assert that both are read in order.
  4. Explain why read_content() should not be used to build the assistant content that is sent back to the API.
Show answers for steps 2 and 4

Step 2: add citations_count: int = 0 to ResponseView, then in the text branch:

view.citations_count += len(field_of(block, "citations") or [])

field_of(block, "citations") or [] treats None and missing citations as an empty list.

Step 4: ResponseView is a summary for reading. It drops thinking content and signatures, merges text segments, and discards block order. Sending a reconstruction back to the API would lose context, and in tool-use conversations it would break the requirement that thinking blocks from the latest assistant turn be returned unchanged. Always send the original message.content (or its stored dictionary form) back to the API.

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

Signed-in readers only.