Understand Message Blocks

Su Suriya Ravichandran Updated 10 Nov 2026
11 min read ·Lesson 16 of 200

What a Content Block Is

A content block is one typed piece of a message. Instead of representing a message as a single string, the Claude API represents it as a list of blocks. Each block has a type that says what it contains.

"content": [
  {"type": "thinking", "thinking": "", "signature": "EuYB..."},
  {"type": "text", "text": "Django, Flask, and FastAPI are popular choices."}
]

You have already met blocks in Lessons 11, 14, and 15, where you filtered message.content for text blocks. This lesson explains the full model: which block types exist, what each contains, why the API is designed this way, and how to handle blocks correctly in both responses and requests.


Why Messages Are Made of Blocks

A plain string can only hold text. Real interactions with Claude contain much more:

ContentExample
TextThe answer itself
ReasoningClaude's thinking before the answer
Images and documentsA photo or PDF you send for analysis
Tool callsClaude asking your program to run a function
Tool resultsYour program returning that function's output
CitationsText linked to the source passages it came from

Blocks represent all of these in one consistent structure:

  • Mixed content fits naturally. A single user message can contain an image followed by a question. A single reply can contain reasoning, then text, then a tool call.
  • Order is preserved. Blocks appear in the order they were produced or sent.
  • Code can branch on type. Your program handles each kind of content appropriately, for example showing text, running tools, and hiding thinking.
  • New features add new block types without changing the structure of existing ones. Code that filters by type keeps working.

Blocks in Responses

These are the block types you can find in message.content:

TypeKey fieldsWhen it appearsCovered in
texttext, citationsEvery normal answerThis lesson, Lesson 157
thinkingthinking, signatureWhen the model reasons before or between outputsThis lesson, Lessons 151–153
redacted_thinkingdataWhen part of the reasoning is returned in encrypted formLesson 153
tool_useid, name, inputWhen Claude calls a tool you definedLessons 81–90
server_tool_useid, name, inputWhen Claude uses a server-side tool, such as web searchLesson 155
Server tool result blocks (for example, web_search_tool_result)Tool-specificResults of server-side toolsLesson 155

In a basic request with no tools, you will see text and thinking blocks.

Text Blocks

block.type        # "text"
block.text        # The text content
block.citations   # None, or a list of citations when citations are enabled

A response can contain more than one text block. With citations enabled, for example, the answer is split into segments, each with its own sources. Always combine all text blocks rather than taking the first one (Lesson 15).

Thinking Blocks

block.type        # "thinking"
block.thinking    # The reasoning text; empty by default
block.signature   # An opaque value used to verify the block

What they are: current Claude models use adaptive thinking. Before answering, and sometimes between steps such as tool calls, the model may reason through the problem. On Claude Opus 5.5 and Fable 5.1, thinking is always on.

Why thinking is usually empty: by default, the reasoning text is omitted from the response. The block is present, but block.thinking is an empty string. You can request a summarized version of the reasoning:

message = client.messages.create(
    model="claude-opus-5-5",
    max_tokens=4096,
    thinking={"type": "adaptive", "display": "summarized"},
    messages=[{"role": "user", "content": "Is 391 a prime number?"}],
)

for block in message.content:
    if block.type == "thinking":
        print("THINKING:", block.thinking)
    elif block.type == "text":
        print("ANSWER:", block.text)

Example output:

THINKING: I check divisibility by small primes up to the square root of 391, which is about 19.8. 391 = 17 × 23, so it is not prime.
ANSWER: No. 391 is not prime: it equals 17 × 23.

What the signature is for: it lets the API verify that a thinking block was produced by the model and has not been altered. This matters when you send blocks back in later requests, as in tool-use loops.

Billing: you are billed for the full thinking the model performed, which is reported in usage.output_tokens, not for the shorter summary you see.

Warning: Thinking content is the model's working process, not a polished answer. Do not show it to end users by default. If you display it, label it clearly as reasoning.

Tool Use Blocks (Preview)

When you give Claude tools (Lessons 81–90), a response can contain:

block.type    # "tool_use"
block.id      # Unique ID of this tool call, e.g. "toolu_01A..."
block.name    # The tool to run, e.g. "get_weather"
block.input   # The arguments, as a dict: {"city": "Chennai"}

The response's stop_reason is then "tool_use". Your program runs the tool and sends the result back in a tool_result block. That loop is covered in detail later. For now, recognize that a response's content can be an instruction for your code, not only text for a user.


Handling Every Block Type

Because message.content can contain different types, robust code handles each type explicitly. Python's match statement (available since Python 3.10, which the SDK requires) expresses this clearly:

for block in message.content:
    match block.type:
        case "text":
            print(block.text)
        case "thinking":
            pass  # Usually not shown to users
        case "redacted_thinking":
            pass  # Encrypted reasoning; nothing to display
        case "tool_use":
            print(f"[Tool requested: {block.name} with {block.input}]")
        case _:
            print(f"[Unhandled block type: {block.type}]")

How it works:

  • match block.type: compares the block's type string against each case.
  • Each case handles one type.
  • case _: is the wildcard. It matches anything not handled above. That makes the code forward-compatible: when a future feature introduces a new block type, your program reports it instead of crashing or silently ignoring it.

You can also check types with isinstance and the classes in anthropic.types:

from anthropic.types import TextBlock, ThinkingBlock

for block in message.content:
    if isinstance(block, TextBlock):
        print(block.text)
    elif isinstance(block, ThinkingBlock):
        pass

Both approaches work. Checking block.type is shorter and needs no extra imports. isinstance lets type checkers know the block's exact class inside each branch, which improves editor autocomplete.


Blocks in Requests

Blocks are not only for responses. The content you send is also made of blocks.

The String Shorthand

So far, every user message has used a string:

{"role": "user", "content": "What is Python?"}

This is shorthand for a single text block. The API treats it exactly the same as:

{"role": "user", "content": [{"type": "text", "text": "What is Python?"}]}

Use the string form for plain text, and switch to the list form when a message needs several blocks or non-text content.

Several Blocks in One Message

messages = [{
    "role": "user",
    "content": [
        {"type": "text", "text": "Here is a function:\n\ndef add(a, b): return a + b"},
        {"type": "text", "text": "Add a docstring and type hints."},
    ],
}]

Separate blocks are useful when parts of a message come from different sources, such as a fixed instruction and variable data. They are also needed for features that apply settings to specific blocks, such as prompt caching (Lessons 121–130).

Images and Documents (Preview)

Non-text inputs are sent as their own blocks:

messages = [{
    "role": "user",
    "content": [
        {
            "type": "image",
            "source": {"type": "url", "url": "https://example.com/chart.png"},
        },
        {"type": "text", "text": "Summarize the trend in this chart."},
    ],
}]

The image block comes first and the question second, mirroring how a person would show a picture and then ask about it. Lessons 111–120 cover images, PDFs, and files in depth.

Request Block Types

TypeSent inPurposeCovered in
textuser, assistantPlain textThis lesson
imageuserAn image, by URL, base64 data, or file IDLessons 111–113
documentuserA PDF or text documentLessons 114–118
tool_resultuserThe output of a tool Claude asked you to runLesson 87
Blocks from a previous response (thinking, tool_use, …)assistantSending Claude's earlier turn back in a conversationLessons 24, 88

Sending Response Blocks Back

In multi-turn conversations, you send Claude's previous replies back as assistant messages. The safest way is to send the content blocks exactly as you received them:

history = [{"role": "user", "content": "Is 391 a prime number?"}]

reply = client.messages.create(
    model="claude-opus-5-5",
    max_tokens=4096,
    messages=history,
)

# Append Claude's reply exactly as returned: all blocks, unchanged
history.append({"role": "assistant", "content": reply.content})
history.append({"role": "user", "content": "What are its factors?"})

follow_up = client.messages.create(
    model="claude-opus-5-5",
    max_tokens=4096,
    messages=history,
)

Why send the blocks unchanged:

  • The SDK accepts the response block objects directly, so no conversion is needed.
  • Some features require blocks to be returned exactly as received. In tool-use workflows, the API rejects requests in which thinking blocks from the latest assistant turn were edited, reordered, filtered out, or reconstructed. Their signature is checked.
  • Keeping the original blocks preserves everything the model produced, so it has full context on the next turn.

Lesson 24 (Send Conversation History) builds on this.


Practical Example: A Block Explorer

This script sends a question with summarized thinking enabled and prints a labeled breakdown of every block in the response. It is a useful learning and debugging tool.

"""block_explorer.py: show every content block in a Claude response."""

import sys
import textwrap

from anthropic import Anthropic
from anthropic.types import Message


def preview(text: str, width: int = 70) -> str:
    """Shorten long text to a single readable line."""
    single_line = " ".join(text.split())
    return textwrap.shorten(single_line, width=width, placeholder=" …") or "(empty)"


def explore(message: Message) -> None:
    print(f"stop_reason={message.stop_reason}  blocks={len(message.content)}\n")
    for index, block in enumerate(message.content):
        match block.type:
            case "text":
                print(f"[{index}] text      : {preview(block.text)}")
            case "thinking":
                print(f"[{index}] thinking  : {preview(block.thinking)}")
            case "redacted_thinking":
                print(f"[{index}] redacted  : (encrypted reasoning)")
            case "tool_use":
                print(f"[{index}] tool_use  : {block.name}({block.input})")
            case _:
                print(f"[{index}] {block.type}: (not handled by this explorer)")


if __name__ == "__main__":
    question = " ".join(sys.argv[1:]) or "A train leaves at 9:40 and arrives at 13:15. How long is the trip?"

    client = Anthropic()
    reply = client.messages.create(
        model="claude-opus-5-5",
        max_tokens=4096,
        thinking={"type": "adaptive", "display": "summarized"},
        messages=[{"role": "user", "content": question}],
    )
    explore(reply)
python block_explorer.py

Example output:

stop_reason=end_turn  blocks=2

[0] thinking  : From 9:40 to 13:40 is 4 hours; 13:15 is 25 minutes earlier, so 3 hours …
[1] text      : The trip takes 3 hours and 35 minutes.

How It Works

  • thinking={"type": "adaptive", "display": "summarized"} keeps adaptive thinking, which is the default, but asks for a summary of the reasoning instead of an empty thinking field. Remove it to see the default behavior, where the thinking preview shows (empty).
  • enumerate(message.content) provides each block's index, showing the order of blocks.
  • preview() collapses whitespace with " ".join(text.split()) and shortens long text with textwrap.shorten. The or "(empty)" makes empty thinking visible instead of printing nothing.
  • match block.type handles each known type, and case _ reports anything unexpected.

Run it with different questions. Simple questions may produce no thinking block at all, while multi-step problems usually do.


Common Mistakes

Assuming the First Block Is Text

What happens: message.content[0].text raises AttributeError whenever a thinking block comes first.

Fix: filter by block.type.

Assuming There Is Exactly One Text Block

What happens: parts of the answer are lost when the response contains several text blocks.

Fix: join all text blocks.

Treating an Empty thinking Field as an Error

What happens: developers think thinking is broken because block.thinking is "".

Fix: that is the default. Request "display": "summarized" if you need the reasoning text.

Modifying or Dropping Blocks When Sending Them Back

What happens: in tool-use workflows, requests fail with a 400 error saying thinking blocks in the latest assistant message cannot be modified.

Fix: send reply.content back exactly as received.

Showing Thinking to End Users Unintentionally

What happens: users see internal reasoning mixed with the answer.

Fix: display only text blocks by default.

Not Handling Unknown Block Types

What happens: code crashes, or silently loses content, when a new feature introduces a new block type.

Fix: include a default branch (case _ or else) that logs unexpected types.


Best Practices

  • Always branch on block.type, never on position.
  • Combine all text blocks when you need the answer as a string.
  • Hide thinking by default. Request summarized thinking only when you have a real use for it, such as debugging or transparent reasoning displays.
  • Return assistant content unchanged when continuing a conversation.
  • Handle unknown types gracefully with a default branch, so your code is ready for new features.
  • Use the string shorthand for simple text messages, and lists of blocks when content is mixed.

Exercise

  1. Run block_explorer.py with three questions: a greeting, a factual question, and a multi-step math problem. When do thinking blocks appear?
  2. Remove thinking={...} from the request and run it again. How does the thinking preview change?
  3. Send a user message as a list of two text blocks: one containing a short Python function, and one asking for a review. Confirm that Claude treats them as one message.
  4. Build a two-turn conversation using reply.content as the assistant message, as shown in Sending Response Blocks Back. Print the block types of both replies.
Show answers
  1. Simple greetings often produce no thinking block. Multi-step problems usually start with one. The model decides adaptively.
  2. The thinking block, when present, shows (empty), because the thinking text is omitted by default.
  3. Example:
reply = client.messages.create(
    model="claude-opus-5-5",
    max_tokens=2048,
    messages=[{
        "role": "user",
        "content": [
            {"type": "text", "text": "def area(r): return 3.14 * r * r"},
            {"type": "text", "text": "Review this function and suggest improvements."},
        ],
    }],
)
print("".join(block.text for block in reply.content if block.type == "text"))

Claude reviews the function, because both blocks belong to the same user turn.

  1. Each reply prints its own list of block types, such as ['thinking', 'text'] or ['text']. The second request succeeds because the first reply's blocks were sent back unchanged.

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

Signed-in readers only.