Tool use in Claude: the full tool-calling loop in Python
Tool use lets Claude call functions in your code. The whole loop: tool definitions, parallel calls, errors and limits.
4 min · · development · агенты
Tool use (also called function calling) lets Claude call functions in your code: the model decides which function to call and with which arguments, you run it and return the result, and the model continues. Every agent is built on this, from a calculator to a bot with database access. Below is a complete working example of tool use in Claude in Python with the Anthropic SDK and Zukko, checked against the Anthropic documentation.
How the tool use loop works
- You pass a list of tools in the request: name, description and a JSON Schema for the arguments.
- If Claude decides to call a tool, the response comes with
stop_reason: "tool_use"and one or moretool_useblocks. - You append the model's response to the history in full (
response.content, not just the text). - You execute every
tool_useblock and send the results in one user message, astool_resultblocks carrying the matchingtool_use_id. - You repeat the request. The loop ends when
stop_reasonis no longertool_use.
Install and client
pip install anthropicimport json
import anthropic
client = anthropic.Anthropic(
base_url="https://api.zukko.pro",
api_key="fetch_your_key",
)The address has no /v1: the Anthropic SDK adds the path itself. More in the Python guide and the Messages reference.
Functions and their definitions
First, plain Python functions. The calculator parses the expression with ast rather than eval: arguments come from the model, and so indirectly from the user, and must never be executed as code.
import ast
import operator
def get_weather(location: str, unit: str = "celsius") -> str:
data = {"London": 15, "Paris": 18, "Tokyo": 22}
if location not in data:
raise ValueError(f"No weather data for {location}")
t = data[location]
return f"{location}: {t}°C" if unit == "celsius" else f"{location}: {round(t * 9 / 5 + 32)}°F"
OPS = {ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul,
ast.Div: operator.truediv, ast.Pow: operator.pow, ast.USub: operator.neg}
def _eval(node):Now the definitions for the model. The description is the main thing Claude uses to decide when to call a tool, so make it detailed.
tools = [
{
"name": "get_weather",
"description": "Returns the current temperature in a city. Use it when asked about the weather.",
"input_schema": {
"type": "object",
"properties": {
"location": {"type": "string", "description": "City name in English, e.g. London"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
},
"required": ["location"],
},
},
{The full loop
The key detail: in one response Claude may request several tools at once (for example, the weather in two cities). You must execute all calls and return all results in one user message. If you handle only the first tool_use block, the API rejects the next request: every call needs its own tool_result.
def run_agent(user_message: str, model: str = "claude-sonnet-5", max_steps: int = 10) -> str:
messages = [{"role": "user", "content": user_message}]
for _ in range(max_steps):
response = client.messages.create(
model=model,
max_tokens=4096,
tools=tools,
messages=messages,
)
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason != "tool_use":
if response.stop_reason == "max_tokens":What matters here:
- The model's response is appended in full. Besides
tool_use,response.contentmay contain text and other blocks; return them to the history unchanged. - Results come first. In the message with results,
tool_resultblocks must be at the start; if you want to add text, put it after all results. - Errors are not lost. If a function fails, return a
tool_resultwithis_error: Trueand the error text. The model will see it and can fix the arguments or tell the user. - There is a step limit.
max_stepsprotects against infinite loops and unexpected spending.
Running it
print(run_agent("What's the weather in London and Paris? And what is (18 - 15) * 7?"))
print(run_agent("How many degrees Fahrenheit is it in Tokyo?"))
print(run_agent("What's the weather in Berlin?"))The third request gets an error from the tool (no data), and the model reports it in its answer instead of crashing.
Controlling tool choice
tool_choice defaults to {"type": "auto"}: the model decides whether to call a tool. You can disable calls ({"type": "none"}) or limit them to one per turn: {"type": "auto", "disable_parallel_tool_use": True}.
Forced calls ({"type": "any"} or {"type": "tool", "name": "..."}) are not supported by every model. According to the Anthropic docs, these values return a 400 error for Claude Opus 5.5. If you need a guaranteed call with that model, use auto plus an explicit instruction in the prompt, such as "use the get_weather tool".
Async version
import asyncio
from anthropic import AsyncAnthropic
aclient = AsyncAnthropic(base_url="https://api.zukko.pro", api_key="fetch_your_key")
async def run_agent_async(user_message: str, model: str = "claude-sonnet-5") -> str:
messages = [{"role": "user", "content": user_message}]
for _ in range(10):
response = await aclient.messages.create(
model=model, max_tokens=4096, tools=tools, messages=messages,
)
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason != "tool_use":
return "".join(b.text for b in response.content if b.type == "text")Common mistakes
- Only the first
tool_useis handled. With parallel calls the next request fails with 400. Iterate over all blocks. - Only the response text is added to history. Append
response.contentin full. - Results are split across several messages. All
tool_resultblocks for one turn go into one user message. - Missing
tool_use_id. Without it the model cannot match the result to the call. - Arguments are executed as code. No
eval,exec, or unchecked substitution into shell or SQL.
Each loop step is a separate request, and the whole history is billed again as input tokens. For agents with a long history, remember that Zukko bills cached tokens at the regular input price. You can create a key for experiments in the dashboard.
FAQ
Can Claude skip calling a tool?
Yes. If no tool is needed, the model answers right away with stop_reason: "end_turn". The loop above handles this: it returns the text as soon as stop_reason is not tool_use.
Can Claude call several tools in one turn?
Yes, that is the default. Execute all calls and return the results in one message. You can limit it to one call per turn with disable_parallel_tool_use.
Is there a limit on tool result size?
The result becomes part of the context and is billed as input tokens on every following step. Return only what is needed: filtered rows, not the whole response of an external API.
Can I do the same with the OpenAI format?
Yes, but tool definitions and the response format differ there. This example is for the Anthropic SDK; for the OpenAI format see the Chat Completions reference.
zukko
Pay 25×
less
for Claude Code & ChatGPT
One fetch_* key and base URL api.zukko.pro — for Cursor, Claude Code, Codex, and any SDK.
Related articles
- Prompt Injection: What It Is and How to Protect a Bot and an AgentHow prompt injection works through user input and third-party documents, and which measures limit the damage: data isolation, tool permissions, key limits.
- Claude Code YOLO: Permission Modes and Working Without PromptsClaude Code has no mode literally called YOLO, but it has several permission modes and flexible rules. How to stop clicking Yes without losing files.
- Claude Code: a checklist of best practices for daily workHabits that make working with Claude Code predictable: a short CLAUDE.md, a plan before edits, small tasks, checking results and keeping spend under control.
- An MCP server in Java: a minimal example with one toolWe write a minimal MCP server in Java with one tool, register it in Claude Code, and learn to find the cause when the agent doesn't see it.