Tool Use Best Practices
Numbered practices for schema design, error handling, and multi-turn tool loops - a section-wide summary you can use as a checklist before shipping a tool-use integration built on the official anthropic Python SDK.
How to Use This List
- Read top to bottom the first time - schema design and tool_choice come first because they affect whether Claude calls the right tool at all, before you get to loop mechanics.
- Treat items 1-2 as design-time work (done once per tool), items 3-6 as request-time and loop-time behavior (done on every call), and items 7-9 as scaling concerns you adopt once the tool set or traffic grows.
- Come back to this page whenever you add a new tool or see Claude picking the wrong tool, dropping a
tool_result, or looping longer than expected. - Pair this with the deeper pages linked in Related - this page is deliberately terse; the linked pages carry the full explanations and worked examples.
A - Schema Design
- Give every tool a clear, descriptive name. Use
get_weather, notwortool1- Claude matches yourdescriptionagainst the user's intent, and a vague name gives it less to work with. - Write descriptions that say WHEN to call the tool, not just what it does. "Call this when the user asks about current prices" out-performs a description that only states functionality - recent Claude models reach for tools more conservatively by default, so a prescriptive, trigger-worded description measurably improves should-call accuracy.
- Give every
input_schemaproperty its owndescription. Claude reads per-property descriptions to decide what value to extract and how to format it - an undocumented property is a property Claude has to guess about. - Use
enumfor fixed-choice parameters. If a parameter only ever takes one of a known set of values (e.g."unit": ["celsius", "fahrenheit"]), constrain it withenuminstead of leaving it as free-form text. - Mark only truly required fields in
required. Over-marking fields as required forces Claude to either fabricate a value or ask an unnecessary clarifying question when the field doesn't apply. - Set
"strict": truewhen you need exact schema validation. Add it at the top level of the tool definition alongsideadditionalProperties: falseandrequired- this makestool_use.inputvalidate exactly against the schema instead of drifting on edge cases.
B - Keep the Tool Set Focused
- Don't register more tools than the task needs. Too many tools, vague names, or overlapping descriptions can cause Claude to pick the wrong tool or skip calling one entirely - a large undifferentiated tool set is worse for accuracy than a small, well-described one.
- Check for description overlap before adding a new tool. If two tools could plausibly both satisfy the same request, tighten the "call this when" language on each one until they're mutually distinguishable.
C - tool_choice
- Default to
{"type": "auto"}for general-purpose agents. Let Claude decide whether to call a tool and which one, based on the conversation. - Use
{"type": "any"}when the turn must end in some action but you don't care which tool. This guarantees a tool call without picking on Claude's behalf. - Use
{"type": "tool", "name": "..."}to force one specific tool. Reach for this when the workflow requires a particular function call regardless of what Claude would otherwise choose. - Use
{"type": "none"}to temporarily suppress tool calls without droppingtoolsfrom the request. Removing tool definitions entirely invalidates prompt caching -nonekeeps the definitions in place and just stops that one turn from calling a tool. - Add
disable_parallel_tool_use: trueon anytool_choicevalue when your code can't handle more than one tool call per turn. It works alongsideauto,any,tool, ornoneand caps Claude to a single tool call in that response.
response = client.messages.create(
model="claude-sonnet-5",
max_tokens=1024,
tools=[weather_tool, calculate_tool],
tool_choice={"type": "auto", "disable_parallel_tool_use": True},
messages=[{"role": "user", "content": "What's the weather in Austin, TX?"}],
)D - Error Handling in tool_result Blocks
- Return
is_error: trueon tool failure instead of dropping the result or raising. Claude can then retry the call, try a different tool, or ask the user for clarification - a missingtool_resultor an unhandled exception leaves the conversation in a broken state.
def run_tool(tool_use):
try:
output = execute(tool_use.name, tool_use.input)
return {
"type": "tool_result",
"tool_use_id": tool_use.id,
"content": str(output),
}
except Exception as exc:
return {
"type": "tool_result",
"tool_use_id": tool_use.id,
"content": f"Error: {exc}",
"is_error": True,
}E - Multi-Turn Loop Hygiene
- Append the full
response.contentback intomessages, not just extracted text. The API needs thetool_useblocks preserved in the assistant turn - stripping them down to text-only content breaks the next request. - Guard every loop with a
max_iterationscap. A tool-use loop with no ceiling can run away if Claude keeps calling tools without producing a final answer. - Handle
stop_reason == "pause_turn"by resending the conversation as-is. This stop reason means a server-side tool hit its internal iteration limit - don't inject an extra "Continue" message; the API resumes automatically because it detects the trailingserver_tool_useblock.
messages = [{"role": "user", "content": "What's the weather in Austin, TX?"}]
for _ in range(max_iterations := 8):
response = client.messages.create(
model="claude-sonnet-5",
max_tokens=1024,
tools=[weather_tool],
messages=messages,
)
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason == "pause_turn":
continue # resend as-is, no injected "Continue" message
if response.stop_reason != "tool_use":
break
tool_results = [run_tool(block) for block in response.content if block.type == "tool_use"]
messages.append({"role": "user", "content": tool_results})F - Parallel Tool Calls
- Expect and handle multiple
tool_useblocks in one turn. Claude may request several tools in parallel by default - execute them concurrently where practical instead of assuming exactly one call per turn. - Return ALL
tool_resultblocks together in a singleusermessage. Never split them across multiple messages - doing so can degrade Claude's future parallel-call behavior. - Match every
tool_use_idClaude sent with atool_result, including failed calls. Atool_useblock with no correspondingtool_result(even anis_error: trueone) leaves the turn incomplete.
G - Scaling to Many Tools
- Adopt the Tool Search Tool once a tool library grows into the dozens-to-hundreds range. Use
tool_search_tool_regex_20251119ortool_search_tool_bm25_20251119withdefer_loading: trueon rarely-used tools to cut token usage roughly 85%. - Keep the search tool itself and at least one other tool non-deferred. Deferring every tool, including the search tool, makes the request 400 - Claude needs at least one immediately-loaded tool to reason with.
- Prefer the MCP connector over a client-side wrapper for remote/hosted capabilities. When a suitable MCP server already exists, use
mcp_serversplus a matchingmcp_toolsetentry (betamcp-client-2025-11-20) instead of reimplementing a client that calls the same remote API.
H - Prompt Caching With Tools
- Remember that tool definitions render first in the prompt, before system and messages. Adding, removing, or reordering tools invalidates the tools/system cache tier - toggling
tool_choicealone does not. - Keep tool ordering deterministic if you build the
toolslist dynamically. Sort by name (or another stable key) so the same tool set always serializes in the same order and doesn't needlessly bust the cache.
I - JSON Parsing
- Always read
tool_use.inputvia the SDK's parsed object, never by raw-string-matching it. Recent Claude models may escape Unicode or forward slashes differently in generated JSON, so string matching against the raw payload is fragile in ways a parsed object isn't.
FAQs
Which practice on this page matters most for should-call accuracy?
Schema design, specifically writing descriptions that state WHEN to call a tool rather than only what it does. Recent Claude models are more conservative by default about reaching for tools, so a prescriptive, trigger-worded description has an outsized effect.
Do I need `"strict": true` on every tool?
No. Use it when you need tool_use.input to validate exactly against the schema - for example, feeding the output straight into a strongly-typed function. For looser use cases it's optional.
Why does the list put schema design before tool_choice and loop mechanics?
Schema design is design-time work that determines whether Claude picks the right tool at all. tool_choice and loop hygiene are request-time behavior that only matter once the right tool is already in play.
What's the single biggest mistake in multi-turn loops?
Appending only the extracted text of response.content back into messages instead of the full content list. That silently drops the tool_use blocks the API needs on the next turn.
What should I do when stop_reason is "pause_turn"?
Resend the conversation as-is. pause_turn means a server-side tool hit its internal iteration limit - the API detects the trailing server_tool_use block and resumes automatically, so injecting an extra "Continue" message is unnecessary.
Can I just drop a tool_result if the tool call failed?
No. Return a tool_result with is_error: true and a message describing the failure. Dropping the result or raising an exception leaves the turn incomplete and Claude can't retry or ask for clarification.
Do I need to run parallel tool calls concurrently, or can I run them one at a time?
Concurrency is a "where practical" recommendation for latency, not a correctness requirement. What is a correctness requirement is returning all the resulting tool_result blocks together in a single user message, whether you executed them concurrently or sequentially.
When should I reach for the Tool Search Tool instead of just registering all my tools normally?
Once your tool library is in the dozens-to-hundreds range. Below that, the token savings from defer_loading aren't worth the added complexity - just register the tools directly.
Why does deferring every tool cause a 400 error?
The Tool Search Tool and at least one other tool must stay non-deferred so Claude has something to reason with immediately. Deferring the search tool itself along with everything else leaves nothing loaded for the model to act on.
Should I build my own client wrapper around a remote API if an MCP server for it already exists?
Generally no. Prefer the MCP connector (mcp_servers plus a matching mcp_toolset entry) over reimplementing a client-side wrapper around the same remote capability - it avoids duplicating maintenance for something the MCP server already handles.
Does changing tool_choice between requests hurt my prompt cache hit rate?
No. Tool definitions are what render first in the prompt and drive the tools/system cache tier - adding, removing, or reordering tools invalidates it. Toggling tool_choice alone does not.
Why should I sort a dynamically-built tools list?
Because tool definitions render first in the prompt and any change to their order invalidates the tools/system cache tier. Sorting by name (or another stable key) keeps the serialized order identical across requests that use the same tool set, so caching survives.
Why not just check tool_use.input with a raw string match instead of the SDK's parsed object?
Recent Claude models may escape Unicode or forward slashes differently when generating JSON. A raw string match can miss a value that's semantically identical but escaped differently, while the SDK's parsed object handles that decoding for you.
Related
- Tool Use Basics - the full request/response round trip these practices assume
- Defining Tool Schemas with name, description, and input_schema - the deep dive behind the schema-design items above
- Tool Choice Options Reference - full reference for the four
tool_choicemodes - Building a Multi-Turn Tool Use Loop - the worked example behind the loop-hygiene items above
- Executing Parallel Tool Calls in a Single Turn - the full explanation of parallel
tool_usehandling - Scaling to Hundreds of Tools with the Tool Search Tool - the deep dive behind the scaling item above
Stack versions: Written against the Claude model lineup current as of ~June 2026 - Claude Fable 5, Claude Opus 4.8, Claude Sonnet 5 (the default), and Claude Haiku 4.5 - and the official
anthropicPython SDK (latest 0.x release). Model names, SDK versions, and pricing move quickly - verify current specifics at platform.claude.com/docs before relying on them.