Tool Use & Function Calling
Extend Claude with callable functions to take real-world actions.
Introduction
Language models are fundamentally pattern-matching engines trained on text. They are excellent at reasoning, summarising, and generating language — but they cannot browse the web, run computations, or persist data. Tool use bridges this gap by giving Claude a structured way to request that your code performs actions on its behalf.
The Anthropic API implements tool use as a conversation protocol. You declare what tools are available, Claude decides when to call them, your code executes the actual work, and the results flow back into the conversation. Claude's text generation resumes once it has the information it needs.
This pattern is sometimes called "function calling" (as named by OpenAI) or "tool use" (Anthropic's terminology). The concepts are identical: structured requests from the model to invoke external code.
How Tool Use Works
At the protocol level, tool use adds two new content block types to the messages API:
tool_use— emitted by the assistant, containsid,name, andinput(the arguments)tool_result— sent by the user (your code), containstool_use_idandcontent(the result string)
When you call client.messages.create() with a tools parameter, you give Claude a menu of available functions. Claude reads the tool names and descriptions to determine whether any tool is appropriate. If it decides to use one, it sets stop_reason = "tool_use" and includes one or more tool_use blocks in its response content.
Your application must detect this stop reason, extract the tool calls, execute them, and return the results. The conversation then continues — Claude uses the results to formulate its final answer.
Designing Tool Schemas
Each tool is described by a dict with three keys:
- name — a short, snake_case identifier (e.g.
get_weather) - description — plain English explanation of what the tool does, when to use it, and any important caveats
- input_schema — a JSON Schema object describing the arguments Claude should provide
The description is the most important field. Claude uses it at inference time to decide whether this tool is appropriate for the current request. A vague description like "does math" will lead to either over-use or under-use. A good description specifies the exact type of input, the returned format, and edge cases: "Evaluates arithmetic expressions on two numeric operands. Returns a floating-point result. Use this for any calculation requiring precision."
unit_converter_tool = {
"name": "unit_converter",
"description": (
"Convert a numeric value from one unit of measurement to another. "
"Supports: length (m, km, ft, miles), weight (kg, g, lb, oz), "
"temperature (celsius, fahrenheit, kelvin). "
"Returns the converted value as a float."
),
"input_schema": {
"type": "object",
"properties": {
"value": {"type": "number", "description": "The numeric value to convert"},
"from_unit": {"type": "string", "description": "Source unit (e.g. 'km')"},
"to_unit": {"type": "string", "description": "Target unit (e.g. 'miles')"}
},
"required": ["value", "from_unit", "to_unit"]
}
}
The Tool Execution Loop
The loop pattern is the core implementation detail. Because tool calls may chain — Claude calls tool A, sees the result, then calls tool B — you need a loop rather than a simple if-statement.
import anthropic
client = anthropic.Anthropic()
MODEL = "claude-opus-4-5"
def run_with_tools(user_message: str, tools: list, max_iterations: int = 10) -> str:
messages = [{"role": "user", "content": user_message}]
iterations = 0
while iterations < max_iterations:
response = client.messages.create(
model=MODEL,
max_tokens=1024,
tools=tools,
messages=messages,
)
if response.stop_reason == "end_turn":
# Extract text from the final response
for block in response.content:
if hasattr(block, "text"):
return block.text
return ""
if response.stop_reason == "tool_use":
tool_calls = [b for b in response.content if b.type == "tool_use"]
tool_results = []
for tc in tool_calls:
result = execute_tool(tc.name, tc.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": tc.id,
"content": str(result),
})
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": tool_results})
iterations += 1
return "Max iterations reached without a final answer."
Note the two-step append: you must add the assistant's full content (including tool_use blocks) before adding the tool_result user message. The API validates that every tool_use id has a corresponding tool_result.
Parallel Tool Calls
When a user asks for two independent pieces of information — "What's the weather in London and what's 20% of 85?" — Claude can emit both tool calls in a single response. This is more efficient than making two separate round-trips to the API.
Your loop already handles this correctly if you collect all tool_use blocks from response.content and return all results in the same tool_result message. The key constraint is that every tool_use_id from that turn must have exactly one corresponding tool_result in the reply.
# Executing parallel calls — collect ALL tool_use blocks first
tool_calls = [b for b in response.content if b.type == "tool_use"]
# Then execute each (could be done concurrently with threading/asyncio)
tool_results = []
for tc in tool_calls:
result = execute_tool(tc.name, tc.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": tc.id, # must match the id from the tool_use block
"content": str(result),
})
# Return ALL results in a single user message
messages.append({"role": "user", "content": tool_results})
If your tool functions are I/O bound (hitting external APIs), you can execute them concurrently using concurrent.futures.ThreadPoolExecutor and collect results before sending them back.
Error Handling in Tool Loops
Tools can fail for many reasons: bad inputs, network timeouts, permission errors, or division by zero. There are two layers of error handling to consider:
1. Catch exceptions inside execute_tool()
Wrap your tool execution in a try/except and return an error string. Claude understands natural-language error messages and can inform the user or try an alternative approach.
def execute_tool(name: str, inputs: dict) -> str:
try:
if name == "calculator":
op = inputs["operation"]
a, b = inputs["a"], inputs["b"]
if op == "add": return str(a + b)
if op == "subtract": return str(a - b)
if op == "multiply": return str(a * b)
if op == "divide":
if b == 0: return "Error: cannot divide by zero"
return str(a / b)
return f"Error: unknown tool '{name}'"
except KeyError as e:
return f"Error: missing required parameter {e}"
except Exception as e:
return f"Error: {type(e).__name__}: {e}"
2. Guard against infinite loops
If a tool consistently fails and Claude keeps retrying it, your loop could run indefinitely. The max_iterations parameter provides a hard stop. Ten iterations is a generous limit for most real-world tasks.
Tool Design Best Practices
- One tool per concern. A tool that reads files AND writes files AND lists directories is harder to describe accurately than three separate tools.
- Return strings. The
contentfield of atool_resultis a string. Convert numbers, dicts, and lists to a human-readable string format — Claude can parse text more flexibly than it can arbitrary JSON blobs. - Include units and context in returns. Instead of returning
42.3, return"42.3 miles". This reduces ambiguity in Claude's final answer. - Describe side effects in the tool description. If a tool writes to a file or sends an email, say so explicitly. Claude uses this to warn the user about irreversible actions.
- Use enums for constrained inputs. If an argument only accepts specific values (
"add","subtract"), use a JSON Schemaenum— Claude will only generate valid values. - Test schemas with
tool_choice="any". Force Claude to always call a tool to verify your schema produces valid calls during development.