Right, let’s get our hands dirty with Claude’s Tool Use. This isn’t just about making an API call; it’s about teaching Claude to be your highly capable, slightly pedantic, software intern. The core idea is simple: you define functions (tools) that Claude can call, and it returns the results to you. But the devil, as always, is in the details.

Defining Your Tools: The Schema is the Contract

First, you need to tell Claude what tools are available. You do this by passing a list of tool definitions in the tools parameter. Each tool is a JSON schema that describes a function. This schema is your contract with Claude. Be excruciatingly specific here. Vague contracts lead to confused AIs.

Think of it like briefing a junior dev: “Here’s the function name, here’s exactly what it does, and here’s the precise shape of the data it needs.” Claude will use this description to decide if it should call a tool and how to call it.

Here’s an example of a well-defined tool for getting the weather. Notice the level of detail in the description and the parameters. We’re not messing around.

tools = [
    {
        "name": "get_weather",
        "description": "Get the current weather for a specific location. Use this for live weather data, not forecasts.",
        "input_schema": {
            "type": "object",
            "properties": {
                "location": {
                    "type": "string",
                    "description": "The city and state, e.g. San Francisco, CA or a specific airport code like SFO. Be as specific as possible."
                },
                "unit": {
                    "type": "string",
                    "enum": ["celsius", "fahrenheit"],
                    "description": "The unit of temperature to return. Defaults to celsius if not provided."
                }
            },
            "required": ["location"]
        }
    }
]

The enum for the unit parameter is a pro move. It restricts Claude to only two valid options, preventing it from getting “creative” and asking for “kelvin” or “freedom units” (which, let’s be honest, is just Fahrenheit).

The Dance: Making the Request and Handling the Response

You send your message alongside this list of tools. Claude will then process your request and, if it decides a tool is needed, it won’t give you a final answer. Instead, it will respond with a tool_use block. This is Claude saying, “I can’t do this next part myself, but I know exactly which function to call and with what arguments. You handle the execution.”

Your job, as the orchestrator, is to detect this tool_use, run the actual function in your code, and then send the result back to Claude in a subsequent API call.

Here’s the initial request:

from anthropic import Anthropic

client = Anthropic(api_key="your_api_key")

message = client.messages.create(
    model="claude-3-opus-20240229",
    max_tokens=1024,
    tools=tools,
    messages=[{
        "role": "user",
        "content": "What's the weather like in Tokyo right now? Give me the temperature in Celsius."
    }]
)

The response will look something like this. Notice the stop_reason is tool_use, not end_turn.

{
  "id": "msg_123",
  "content": [{
    "type": "tool_use",
    "id": "toolu_01ABCDef",
    "name": "get_weather",
    "input": {"location": "Tokyo, Japan", "unit": "celsius"}
  }],
  "stop_reason": "tool_use"
}

Executing the Function and Submitting the Result

Now you execute the actual get_weather function (or whatever your backend logic is) using the arguments Claude provided. This is where you connect the AI’s reasoning to the real world.

Once you have the result, you send it back using a special tool_result block. This is crucial: you must reference the exact id from the tool_use block. This is how Claude matches the result to its initial request.

# Your actual function to call a weather API
def execute_get_weather(location, unit="celsius"):
    # ... your code to call a weather API here ...
    return {"temperature": 22, "conditions": "Partly cloudy"}

# Extract the tool call from Claude's response
tool_use = next(block for block in message.content if block.type == 'tool_use')
function_result = execute_get_weather(**tool_use.input)

# Send the result back to Claude for final analysis
second_message = client.messages.create(
    model="claude-3-opus-20240229",
    max_tokens=1024,
    tools=tools, # You must include the tools again!
    messages=[
        {"role": "user", "content": "What's the weather like in Tokyo right now? Give me the temperature in Celsius."},
        {
            "role": "assistant",
            "content": [{"type": "tool_use", "id": tool_use.id, "name": tool_use.name, "input": tool_use.input}]
        },
        {
            "role": "user",
            "content": [{
                "type": "tool_result",
                "tool_use_id": tool_use.id, # This links the result to the request
                "content": str(function_result) # Content must be a string
            }]
        }
    ]
)

print(second_message.content[0].text)
# "The current weather in Tokyo is 22°C and partly cloudy. Quite pleasant!"

Pitfalls and Pro-Tips: Lessons from the Trenches

  • You MUST Include Tools on Every Call: Notice in the second call, we had to pass tools=tools again. If you forget, Claude will have amnesia and won’t remember the tool call it just made, leading to confusion.
  • Tool Results Must Be Strings: The content field in a tool_result must be a string. If your function returns a JSON object, you must json.dumps() it. Forgetting this is a classic rookie mistake that results in a silent, confusing failure.
  • Handle Errors Gracefully: What if your get_weather API is down? You can send an error back in the tool_result. Claude is remarkably good at handling this and will explain to the user that there was a temporary problem, rather than just making up a weather report.
  • Beware of Tool Descriptions That Are Too Broad: A tool named search_the_internet with a description of “Finds information online” is a recipe for disaster. Claude will use it for everything, often unnecessarily. Be specific about the tool’s purpose and limitations.

This back-and-forth might feel a bit cumbersome, but it’s the entire point. It keeps Claude grounded in reality—it can only work with the data your tools actually provide, preventing hallucinations and giving you full control over the actions taken. It’s the difference between a colleague who guesses and one who says, “I don’t know, but let me run the exact right query to find out.”