Claude never executes your code. It returns a structured request to call a function; your app runs it and sends the answer back.
1. Define the tool
tools = [{
"name": "get_weather",
"description": "Get the current weather for a given location.",
"input_schema": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "City and state, e.g. San Francisco, CA",
}
},
"required": ["location"],
},
}]
name, description and input_schema are the only required fields. The description is part of the prompt — write it like documentation, not like a variable name.
2. What comes back
{
"stop_reason": "tool_use",
"content": [
{ "type": "text", "text": "Let me check." },
{
"type": "tool_use",
"id": "toolu_01A09q90qw90lq917835lq9",
"name": "get_weather",
"input": { "location": "San Francisco, CA" }
}
]
}
stop_reason: "tool_use" is the signal to run something. Keep the id — your result has to reference it.
3. The loop
import json
import anthropic
client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY
def get_weather(location):
return {"location": location, "temp_c": 15, "sky": "partly cloudy"}
messages = [{"role": "user", "content": "What's the weather in San Francisco?"}]
while True:
response = client.messages.create(
model="claude-opus-5",
max_tokens=1024,
tools=tools,
messages=messages,
)
if response.stop_reason != "tool_use":
break
messages.append({"role": "assistant", "content": response.content})
results = []
for block in response.content:
if block.type == "tool_use":
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": json.dumps(get_weather(**block.input)),
})
messages.append({"role": "user", "content": results})
print(next(b.text for b in response.content if b.type == "text"))
Runs until Claude stops asking for tools. Two rules that bite people: tool results go back in a user message, and every tool_use block from one turn must be answered in a single message — splitting them teaches the model to stop calling tools in parallel.
Errors and guarantees
{
"type": "tool_result",
"tool_use_id": block.id,
"content": "Error: unknown city 'xyz'. Ask the user for a valid city.",
"is_error": True,
}
Reports a failed call instead of dropping it, so Claude can retry or ask a follow-up question. Add "strict": True to a tool definition (with required and "additionalProperties": false) to guarantee the arguments validate against your schema.
block.input is already a parsed dict — use it as data, never string-match the serialized JSON.