Read Tool Calls

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

What a Tool Call Looks Like

When Claude decides to use a tool, the response contains one or more tool_use content blocks, and stop_reason is "tool_use":

response.stop_reason   # "tool_use"
response.content
# [ThinkingBlock(...),                                   ← optional
#  TextBlock(text="Let me check the weather."),          ← optional preamble
#  ToolUseBlock(id="toolu_01A09q...", name="get_weather", input={"city": "Chennai"})]

Each tool_use block has three important fields:

FieldTypeMeaning
idstrA unique ID for this call, starting with toolu_. Your tool result must reference it (Lesson 87).
namestrWhich tool to run, matching a name from your tool definitions
inputdictThe arguments, following the tool's input_schema

Reading tool calls correctly means finding all of these blocks, checking that each one is something your application can and should run, and turning its input into arguments for your Python function.


Finding Tool Calls

Filter the content blocks by type, as with text (Lesson 42):

tool_calls = [block for block in response.content if block.type == "tool_use"]

for call in tool_calls:
    print(call.id, call.name, call.input)
toolu_01A09q90qw90lq917835lq9 get_weather {'city': 'Chennai'}

Several Calls in One Response

Claude can request several tools in one response, for example the weather in two cities, or enrollments plus a course search (Lesson 84). Always loop over all tool_use blocks. Taking only the first one leaves the others unanswered, and the next request will fail, because every call needs a result (Lesson 87).

Text Alongside Tool Calls

A response may contain text before its tool calls, such as "Let me look that up for you." You can show this text to the user as a status message, but never treat it as the final answer when stop_reason is "tool_use".


Check the Stop Reason First

stop_reasonMeaning for tool calls
tool_useClaude is waiting for tool results. Read and run the calls.
end_turnClaude finished, so there are no pending tool calls.
max_tokensThe response was cut off. If a tool_use block was being generated, its input may be incomplete. Retry with a higher max_tokens. Do not run a possibly incomplete call.
refusalClaude declined. Do not run anything.

Only run tool calls when stop_reason is "tool_use".


Validate Before Running

A tool call is a request from the model. Your application decides whether to honor it. Before running anything, check:

  1. Is the tool known? The name must exist in your registry. With strict: true, the name is always one of your tools (Lesson 83), but defensive code still checks.
  2. Is the input valid? Validate it against the expected structure: types, required fields, and allowed values. Pydantic models do this cleanly (Lesson 83).
  3. Is the call allowed? Check permissions and business rules: may this user perform this action, on this data, right now? Lessons 176–178 cover tool permissions.

Validation problems are not fatal: you can send an error result back to Claude, which then corrects its call (Lessons 87 and 90).


Turning Input into Function Arguments

block.input is a dictionary whose keys match your schema's properties. If your Python function's parameters use the same names, you can pass the dictionary as keyword arguments:

result = get_weather(**call.input)   # Equivalent to get_weather(city="Chennai")

Validating the input with a Pydantic model first gives you a typed object with defaults applied:

arguments = WeatherInput.model_validate(call.input)
result = get_weather(**arguments.model_dump())

Tool Calls in Streams

When streaming (Lessons 53 and 54), tool inputs arrive as partial JSON fragments. Use the complete block from content_block_stop, or from the final message, before running a tool. Never act on a partial input.


Practical Example: Reading and Validating Tool Calls

This module extracts every tool call from a response, checks that the tool is known, validates its input with a Pydantic model, and reports problems without crashing. An offline test with a hand-made response shows all three outcomes: a valid call, an invalid input, and an unknown tool.

"""read_tool_calls.py: extract, check, and validate tool calls from a response."""

from dataclasses import dataclass
from types import SimpleNamespace

from pydantic import BaseModel, ConfigDict, Field, ValidationError


class WeatherInput(BaseModel):
    model_config = ConfigDict(extra="forbid")
    city: str = Field(description="City name, e.g. Chennai")


class CourseSearchInput(BaseModel):
    model_config = ConfigDict(extra="forbid")
    topic: str = Field(description="Subject, e.g. 'Django'")
    level: str | None = Field(default=None, description="Optional level filter")


INPUT_MODELS: dict[str, type[BaseModel]] = {
    "get_weather": WeatherInput,
    "search_courses": CourseSearchInput,
}


@dataclass
class ToolCall:
    id: str
    name: str
    raw_input: dict
    arguments: BaseModel | None     # Validated, typed input; None if there is a problem
    problem: str | None             # Why the call cannot run; None if it can


def read_tool_calls(response) -> list[ToolCall]:
    calls = []
    for block in response.content:
        if block.type != "tool_use":
            continue
        raw = dict(block.input or {})
        model = INPUT_MODELS.get(block.name)
        if model is None:
            calls.append(ToolCall(block.id, block.name, raw, None, f"unknown tool {block.name!r}"))
            continue
        try:
            calls.append(ToolCall(block.id, block.name, raw, model.model_validate(raw), None))
        except ValidationError as error:
            first = error.errors()[0]
            location = ".".join(str(part) for part in first["loc"]) or "input"
            calls.append(ToolCall(block.id, block.name, raw, None, f"invalid {location}: {first['msg']}"))
    return calls


if __name__ == "__main__":
    fake_response = SimpleNamespace(
        stop_reason="tool_use",
        content=[
            SimpleNamespace(type="text", text="Let me check that for you."),
            SimpleNamespace(type="tool_use", id="toolu_01", name="get_weather", input={"city": "Chennai"}),
            SimpleNamespace(type="tool_use", id="toolu_02", name="search_courses", input={"level": "beginner"}),
            SimpleNamespace(type="tool_use", id="toolu_03", name="delete_course", input={"id": 7}),
        ],
    )

    if fake_response.stop_reason == "tool_use":
        for call in read_tool_calls(fake_response):
            status = "OK" if call.problem is None else f"PROBLEM: {call.problem}"
            print(f"{call.id} {call.name}({call.raw_input}) -> {status}")
            if call.arguments is not None:
                print(f"    typed: {call.arguments!r}")

Output:

toolu_01 get_weather({'city': 'Chennai'}) -> OK
    typed: WeatherInput(city='Chennai')
toolu_02 search_courses({'level': 'beginner'}) -> PROBLEM: invalid topic: Field required
toolu_03 delete_course({'id': 7}) -> PROBLEM: unknown tool 'delete_course'

How It Works

  1. INPUT_MODELS maps each tool name to the Pydantic model for its input. It is the reading side of the registry from Lesson 82.
  2. read_tool_calls() visits every block and keeps only tool_use blocks, all of them, not just the first.
  3. dict(block.input or {}) makes a plain copy of the input, and treats a missing input as an empty dictionary.
  4. Unknown tools are reported as problems instead of raising exceptions. With well-defined tools this is rare, but defensive code never runs a tool it does not recognize.
  5. model.model_validate(raw) checks types, required fields, and unknown fields. A valid input becomes a typed object such as WeatherInput(city='Chennai'). An invalid input produces a short problem message built from the first validation error: its location, joined with dots for nested fields, and its message.
  6. ToolCall keeps the call's ID with its outcome. Every call, even a problematic one, needs a matching result in the next request: a real result for valid calls, and an error result explaining the problem for invalid ones (Lessons 87 and 90).
  7. The offline test uses SimpleNamespace objects with the same attributes as real SDK blocks, so the reading logic can be tested without any API calls (Lesson 42).

Common Mistakes

Reading Only the First Tool Call

What happens: parallel calls go unanswered, and the next request fails because results are missing.

Fix: loop over all tool_use blocks.

Running Calls When the Stop Reason Is Not tool_use

What happens: a truncated (max_tokens) response can contain an incomplete tool input, and running it may do the wrong thing.

Fix: only run calls when stop_reason == "tool_use", and retry truncated responses with a higher max_tokens.

Trusting Inputs Blindly

What happens: invalid, unexpected, or unauthorized inputs reach your systems.

Fix: validate structure with Pydantic, and check permissions and business rules.

Parsing the Preamble Text for Instructions

What happens: code depends on wording that varies.

Fix: use tool_use blocks as the only source of tool calls.

Losing the Call ID

What happens: results cannot be matched to calls.

Fix: keep id together with every call through validation and execution.


Best Practices

  • Check stop_reason before reading tool calls.
  • Handle every tool_use block in a response.
  • Validate names and inputs before running anything.
  • Use Pydantic models to turn inputs into typed arguments with defaults.
  • Report problems as data, so they can become error results for Claude.
  • Keep each call's id with its outcome.
  • Test call reading offline with fake responses.

Exercise

  1. Add a get_enrollments entry to INPUT_MODELS with a model that has no fields. Test it with input={} and with input={"user_id": "123"}. What happens in each case, and why is the second one a good thing to reject?
  2. Run a real request from Lesson 84 that produces two parallel tool calls, and pass the response to read_tool_calls().
  3. Change read_tool_calls() to collect all validation errors for a call, not just the first one.
  4. Write a function pending_ids(response) that returns the IDs of all tool calls in a response, which Lesson 87 uses to check that every call received a result.
Show answers for steps 1 and 3

Step 1:

class EnrollmentsInput(BaseModel):
    model_config = ConfigDict(extra="forbid")


INPUT_MODELS["get_enrollments"] = EnrollmentsInput

{} is valid. {"user_id": "123"} fails with Extra inputs are not permitted, because of extra="forbid". Rejecting it is good: your application decides whose enrollments to read, based on the logged-in user. Claude must not be able to choose another user's ID, whether by mistake or because of a prompt injection (Lessons 40 and 176).

Step 3:

except ValidationError as error:
    messages = [
        f"invalid {'.'.join(str(p) for p in e['loc']) or 'input'}: {e['msg']}"
        for e in error.errors()
    ]
    calls.append(ToolCall(block.id, block.name, raw, None, "; ".join(messages)))

Reporting every problem at once lets Claude fix all of them in a single retry.

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 Tool Calls and get answers drawn from it.

Signed-in readers only.