Tool Use
Let Claude call your tools and APIs to take action.
← All modules in this stageThis is the module where Claude stops being a text-completion engine and starts being something that can take action. You define functions; Claude decides when to call them; you run them and feed the results back. It's the foundation under every "AI agent" you've ever heard about.
By the end of this module you'll have
- A working tool-use loop where Claude calls a real Python function and uses the result
- A clear mental model of the tool-call → tool-result → final-answer sequence
- A safe pattern for adding multiple tools without things getting tangled
Time: about 1.5 hours for the basics, ~7 hours with all three notebooks.
Prerequisites: Modules 4 (API basics), 6 (advanced prompting), 7 (building apps).
How tool use actually works
It's a three-step dance, and it's worth seeing once before reading code:
1. You send messages + a list of tool definitions.
2. Claude responds with content blocks. One of them might be a "tool_use"
block: {"name": "get_weather", "input": {"city": "London"}}.
3. YOU run the tool, then send a *new* request with the conversation so far
plus a "tool_result" block. Claude reads the result and answers normally.
You stay in control. Claude never executes anything itself — it just asks.
A working example: Claude calls a Python function
Save as tool_use_demo.py:
from anthropic import Anthropic
from dotenv import load_dotenv
load_dotenv()
client = Anthropic()
# 1) The tool definition Claude sees.
tools = [{
"name": "get_weather",
"description": "Look up the current weather for a city. Returns a short string.",
"input_schema": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "City name, e.g. 'London'."},
},
"required": ["city"],
},
}]
# 2) Your real implementation. Pretend this hits a weather API.
def get_weather(city: str) -> str:
fake = {"London": "13°C, drizzle", "Tokyo": "24°C, clear", "New York": "9°C, windy"}
return fake.get(city, f"No data for {city}")
# 3) The conversation.
messages = [{"role": "user", "content": "What's the weather like in Tokyo right now?"}]
response = client.messages.create(
model="claude-sonnet-4-6", max_tokens=400, tools=tools, messages=messages,
)
# Claude's first reply may contain a tool_use block. Loop until it stops asking.
while response.stop_reason == "tool_use":
# Append Claude's whole turn to the history (the SDK gives us the message back).
messages.append({"role": "assistant", "content": response.content})
tool_results = []
for block in response.content:
if block.type == "tool_use":
result = get_weather(**block.input) # YOUR code runs here
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result,
})
messages.append({"role": "user", "content": tool_results})
response = client.messages.create(
model="claude-sonnet-4-6", max_tokens=400, tools=tools, messages=messages,
)
# After the loop, Claude has stopped asking and has a final answer.
print(response.content[-1].text)
What you should see: Claude responds first with a tool_use block requesting get_weather(city="Tokyo"), your code runs, you send the result back, and Claude replies "It's 24°C and clear in Tokyo right now." — using your data, not its imagination.
What just happened?
Three new concepts are doing all the work:
tools=[...]— you describe each function with a JSON Schema. Claude reads this and decides when (and with what arguments) to call.stop_reason == "tool_use"— the model stopped to ask you to run something. As long as it keeps asking, keep looping.tool_resultblocks — you reply with ausermessage whose content is a list of results, each tagged with thetool_use_idClaude generated. That's how multiple tool calls in one turn stay matched up.
Adding a second tool
Adding tools is mostly mechanical: define the schema, write the function, add to the dispatch table.
tools.append({
"name": "send_email",
"description": "Send a one-line email. Use only when the user explicitly asked to email someone.",
"input_schema": {
"type": "object",
"properties": {
"to": {"type": "string", "format": "email"},
"subject": {"type": "string"},
"body": {"type": "string"},
},
"required": ["to", "subject", "body"],
},
})
DISPATCH = {
"get_weather": get_weather,
"send_email": lambda **kw: f"(pretend) sent: {kw['subject']}",
}
# In the loop:
result = DISPATCH[block.name](**block.input)
Two practical bits:
- Be specific in the description. "Use only when the user explicitly asked to email someone" stops Claude from emailing on its own initiative. Tool descriptions are part of the prompt.
- Validate inputs. Schemas help, but always re-check anything dangerous (file paths, shell commands, money) before executing.
Safety patterns worth knowing
| Risk | Cheap mitigation |
|---|---|
| Claude calls a destructive tool unprompted | Description starts with "Use only when explicitly asked." Plus a code-side check. |
| Tool errors crash the loop | Wrap each call in try/except; pass {"is_error": True, "content": str(exc)} back as the tool_result. Claude will adapt. |
| Infinite tool-use loops | Cap iterations: for _ in range(MAX_TURNS): around the loop. |
| User input bleeds into tool args | Treat tool inputs as untrusted. Re-validate before hitting the network or the filesystem. |
| Sensitive tool needs human approval | Don't auto-execute. Show the proposed call to the user, wait for OK, then run. |
Try changing one thing
- Ask Claude something the tool can't answer (
"What's the weather on Mars?"). Watch how it handles a"No data for Mars"result — it'll usually apologise and explain instead of inventing data. - Add a
tool_choice={"type": "tool", "name": "get_weather"}argument to force Claude to use a specific tool. Useful when you know what you want. - Make the tool deliberately raise an exception. Catch it, return
{"is_error": True, "content": "Service unavailable"}and watch Claude recover gracefully. - Switch to
claude-haiku-4-5-20251001. Tool use works on Haiku too — and it's much cheaper for high-volume routing.
Going deeper: open the notebooks
notebooks/01_introduction.ipynb— the tool-use loop end-to-end, multiple tools, simple agents (~1.5–2h)notebooks/02_intermediate.ipynb— parallel tool calls, observability, integration tests (~2–3h)notebooks/03_advanced.ipynb— abuse threat-modelling, per-tool rate limits, audit trails (~1.5–2.5h)
Module checklist
- [ ] You've watched Claude call a function you wrote and use the result
- [ ] You can describe the tool_use → tool_result → final-answer sequence without notes
- [ ] You know how to handle a tool that errors instead of crashing the loop
- [ ] You've capped your loop with a max iteration count
Next module
Module 9 · RAG Systems — give Claude access to your documents, not just function calls.