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.
Search across all documentation pages
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.
tool_result, or looping longer than expected.get_weather, not w or tool1 - Claude matches your description against the user's intent, and a vague name gives it less to work with.input_schema property its own description. 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.enum for fixed-choice parameters. If a parameter only ever takes one of a known set of values (e.g. "unit": ["celsius", "fahrenheit"]), constrain it with enum instead of leaving it as free-form text.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."strict": true when you need exact schema validation. Add it at the top level of the tool definition alongside additionalProperties: false and required - this makes tool_use.input validate exactly against the schema instead of drifting on edge cases.{"type": "auto"} for general-purpose agents. Let Claude decide whether to call a tool and which one, based on the conversation.{"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.{"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.{"type": "none"} to temporarily suppress tool calls without dropping tools from the request. Removing tool definitions entirely invalidates prompt caching - none keeps the definitions in place and just stops that one turn from calling a tool.disable_parallel_tool_use: true on any tool_choice value when your code can't handle more than one tool call per turn. It works alongside auto, any, tool, or none and 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?"}],
)is_error: true on 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 missing tool_result or 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,
}response.content back into messages, not just extracted text. The API needs the tool_use blocks preserved in the assistant turn - stripping them down to text-only content breaks the next request.max_iterations cap. A tool-use loop with no ceiling can run away if Claude keeps calling tools without producing a final answer.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 trailing server_tool_use block.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})tool_use blocks 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.tool_result blocks together in a single user message. Never split them across multiple messages - doing so can degrade Claude's future parallel-call behavior.tool_use_id Claude sent with a tool_result, including failed calls. A tool_use block with no corresponding tool_result (even an is_error: true one) leaves the turn incomplete.tool_search_tool_regex_20251119 or tool_search_tool_bm25_20251119 with defer_loading: true on rarely-used tools to cut token usage roughly 85%.mcp_servers plus a matching mcp_toolset entry (beta mcp-client-2025-11-20) instead of reimplementing a client that calls the same remote API.tool_choice alone does not.tools list 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.tool_use.input via 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.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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
tool_choice modestool_use handlingStack 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.
Reviewed by Chris St. John·Last updated Jul 19, 2026