Lesson 07 Tool Use & Function Calling
Theme:

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:

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:

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

Further Reading