Let the model call your code — look up data, hit an API, run a calculation — and use the result in its answer. Define a tool, run the request loop, and return the output.
Why: a model cannot fetch live data or do exact math, but it can ask YOU to — you expose functions ("tools"), the model decides when to call them, you run them and hand back the result. When: give the model tools whenever the answer depends on the current world (weather, a database, your API). Where: the model never runs your code; it only requests a call, and your program executes it.
USER ---> MODEL: "What's the weather in Cairo?"
MODEL ---> YOU: call get_weather(city="Cairo")
YOU ---> MODEL: {"temp_c": 34, "sky": "clear"}
MODEL ---> USER: "It's 34°C and clear in Cairo."
The model picks the tool and the arguments. You do the work.Why: the description and schema ARE the model’s instructions — a vague description makes it call the wrong tool or invent arguments. When: write the description as if explaining to a new teammate what the tool does and when to use it. Where: required fields and types in input_schema are enforced by the API.
weather_tool = {
"name": "get_weather",
"description": "Get the current weather for a city. Use this whenever the "
"user asks about current conditions or temperature.",
"input_schema": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "City name, e.g. 'Cairo'"},
},
"required": ["city"],
},
}
def get_weather(city):
# A real implementation would call a weather API here.
return {"city": city, "temp_c": 34, "sky": "clear"}Why: a tool call is a two-step round trip — the model returns stop_reason "tool_use", you run the function, then you resend the conversation with the result so the model can answer. When: loop until stop_reason is no longer "tool_use", because the model may chain several calls. Where: the tool_result must reference the tool_use_id so the model knows which call it answers.
messages = [{"role": "user", "content": "What's the weather in Cairo?"}]
while True:
resp = client.messages.create(
model="claude-opus-4-8", max_tokens=1024,
tools=[weather_tool], messages=messages,
)
messages.append({"role": "assistant", "content": resp.content})
if resp.stop_reason != "tool_use":
break # model gave its final answer
# Run every tool the model asked for and return the results.
results = []
for block in resp.content:
if block.type == "tool_use":
output = get_weather(**block.input)
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": str(output),
})
messages.append({"role": "user", "content": results})
print(resp.content[-1].text)Why: if a tool raises, the loop crashes — instead hand the model a structured error so it can retry, ask the user, or apologize gracefully. When: wrap tool execution in try/except and put the message in tool_result with is_error. Where: this keeps a single failing call from taking down the whole turn.
def run_tool(block):
try:
output = get_weather(**block.input)
return {"type": "tool_result", "tool_use_id": block.id,
"content": str(output)}
except Exception as e:
# The model sees the error and can recover instead of the app crashing.
return {"type": "tool_result", "tool_use_id": block.id,
"content": f"Error: {e}", "is_error": True}