Originally published at kalyna.pro
Function calling — what Anthropic calls tool use — lets Claude call code you write: query a database, hit an internal API, run a calculation, or check today's date. Claude never executes anything itself. It returns a structured request to call a specific tool with specific arguments, your code runs that tool, and you send the result back so Claude can continue. The Claude API Tutorial covers a single-tool example — this guide goes further: multi-step tool loops, parallel tool calls, forcing a specific tool, streaming tool inputs, and error handling, finishing with a complete multi-tool agent you can extend.
Prerequisites
pip install anthropic
export ANTHROPIC_API_KEY="sk-ant-..."
Defining a Tool
A tool definition has three parts: a name, a description, and an input_schema written as JSON Schema. Claude relies entirely on these three fields to decide whether to call the tool and how to fill in its arguments.
tools = [
{
"name": "get_stock_price",
"description": "Get the current price of a stock by its ticker symbol.",
"input_schema": {
"type": "object",
"properties": {
"ticker": {
"type": "string",
"description": "Stock ticker symbol, e.g. 'AAPL' or 'GOOGL'",
},
"currency": {
"type": "string",
"enum": ["USD", "EUR", "GBP"],
"description": "Currency to return the price in. Defaults to USD.",
},
},
"required": ["ticker"],
},
}
]
If Claude decides the tool is needed, the response has stop_reason == "tool_use" and response.content includes a tool_use block with name, input (already parsed as a dict), and a unique id.
The Tool-Use Loop
Tool use is a loop:
- Call
messages.create()withtoolsand the conversation so far - If
stop_reason != "tool_use", Claude is done — return its text - Otherwise, execute every
tool_useblock inresponse.contentand collect the results - Append Claude's response and a new user message containing
tool_resultblocks, then repeat
from anthropic import Anthropic
client = Anthropic()
def get_stock_price(ticker: str, currency: str = "USD") -> str:
prices = {"AAPL": 230.15, "GOOGL": 178.32, "MSFT": 415.50}
price = prices.get(ticker.upper())
if price is None:
return f"No data for ticker '{ticker}'"
return f"{ticker.upper()}: {price} {currency}"
tools = [...] # tool definition from above
messages = [{"role": "user", "content": "What's Apple's stock price?"}]
while True:
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
tools=tools,
messages=messages,
)
if response.stop_reason != "tool_use":
print(response.content[0].text)
break
messages.append({"role": "assistant", "content": response.content})
tool_results = []
for block in response.content:
if block.type == "tool_use" and block.name == "get_stock_price":
result = get_stock_price(**block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result,
})
messages.append({"role": "user", "content": tool_results})
Parallel Tool Calls
Claude can request several tools in a single response — for example, a stock price and an exchange rate together. In that case response.content contains more than one tool_use block, each with its own id.
def get_exchange_rate(from_currency: str, to_currency: str) -> str:
rates = {("USD", "EUR"): 0.92, ("EUR", "USD"): 1.09}
rate = rates.get((from_currency.upper(), to_currency.upper()))
if rate is None:
return f"No rate for {from_currency} -> {to_currency}"
return f"1 {from_currency.upper()} = {rate} {to_currency.upper()}"
tools.append({
"name": "get_exchange_rate",
"description": "Get the exchange rate between two currencies.",
"input_schema": {
"type": "object",
"properties": {
"from_currency": {"type": "string", "description": "3-letter code, e.g. 'USD'"},
"to_currency": {"type": "string", "description": "3-letter code, e.g. 'EUR'"},
},
"required": ["from_currency", "to_currency"],
},
})
The loop from the previous section already handles parallel calls — it iterates over every block and replies with one tool_result per tool_use_id. Order doesn't matter; Claude matches results to calls by tool_use_id.
Controlling Tool Choice
-
{"type": "auto"}— default. Claude decides whether a tool is needed. -
{"type": "any"}— Claude must call one of the provided tools. -
{"type": "tool", "name": "..."}— Claude must call this specific tool. -
{"type": "none"}— Claude must not call any tool.
Forcing a specific tool is a clean way to get structured JSON output:
save_contact_tool = {
"name": "save_contact",
"description": "Save extracted contact information.",
"input_schema": {
"type": "object",
"properties": {
"name": {"type": "string", "description": "Full name of the contact"},
"email": {"type": "string", "description": "Email address"},
"phone": {"type": "string", "description": "Phone number, with country code if present"},
},
"required": ["name", "email"],
},
}
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
tools=[save_contact_tool],
tool_choice={"type": "tool", "name": "save_contact"},
messages=[{
"role": "user",
"content": "Reach out to John Doe at john@example.com, phone +1 555-1234.",
}],
)
contact = response.content[0].input
print(contact)
# {'name': 'John Doe', 'email': 'john@example.com', 'phone': '+1 555-1234'}
You can also pass "disable_parallel_tool_use": True inside tool_choice to guarantee exactly one tool call back.
Streaming Tool Inputs
with client.messages.stream(
model="claude-sonnet-4-6",
max_tokens=1024,
tools=tools,
messages=messages,
) as stream:
for event in stream:
if event.type == "content_block_delta" and event.delta.type == "input_json_delta":
print(event.delta.partial_json, end="", flush=True)
final = stream.get_final_message()
for block in final.content:
if block.type == "tool_use":
print(block.name, block.input)
get_final_message() gives you fully-parsed tool_use blocks once the stream ends — you rarely need to assemble partial_json fragments yourself.
Error Handling in Tool Results
try:
result = get_stock_price(**block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result,
})
except Exception as e:
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": f"Error: {e}",
"is_error": True,
})
Claude sees the error and adapts — retries with different arguments, tries another tool, or explains the failure.
Best Practices for Tool Definitions
- Write descriptions like API docs for a new teammate
- Use
enumto constrain free-text values - Keep
requiredminimal - One tool = one job — avoid mega-tools
- Return concise, structured results — large blobs cost input tokens on every following call
- Name tools with verbs (
get_,search_,create_)
Complete Example: A Multi-Tool Agent
from anthropic import Anthropic
from datetime import datetime, timezone
client = Anthropic()
def get_stock_price(ticker: str, currency: str = "USD") -> str:
prices = {"AAPL": 230.15, "GOOGL": 178.32, "MSFT": 415.50}
price = prices.get(ticker.upper())
if price is None:
return f"No data for ticker '{ticker}'"
return f"{ticker.upper()}: {price} {currency}"
def get_exchange_rate(from_currency: str, to_currency: str) -> str:
rates = {("USD", "EUR"): 0.92, ("EUR", "USD"): 1.09}
rate = rates.get((from_currency.upper(), to_currency.upper()))
if rate is None:
return f"No rate for {from_currency} -> {to_currency}"
return f"1 {from_currency.upper()} = {rate} {to_currency.upper()}"
def get_current_time() -> str:
return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
TOOL_FUNCTIONS = {
"get_stock_price": get_stock_price,
"get_exchange_rate": get_exchange_rate,
"get_current_time": get_current_time,
}
TOOLS = [
{
"name": "get_stock_price",
"description": "Get the current price of a stock by its ticker symbol.",
"input_schema": {
"type": "object",
"properties": {
"ticker": {"type": "string", "description": "Stock ticker, e.g. 'AAPL'"},
"currency": {"type": "string", "enum": ["USD", "EUR", "GBP"], "description": "Defaults to USD"},
},
"required": ["ticker"],
},
},
{
"name": "get_exchange_rate",
"description": "Get the exchange rate between two currencies.",
"input_schema": {
"type": "object",
"properties": {
"from_currency": {"type": "string", "description": "3-letter currency code, e.g. 'USD'"},
"to_currency": {"type": "string", "description": "3-letter currency code, e.g. 'EUR'"},
},
"required": ["from_currency", "to_currency"],
},
},
{
"name": "get_current_time",
"description": "Get the current date and time in UTC.",
"input_schema": {"type": "object", "properties": {}},
},
]
def run_agent(user_message: str, max_steps: int = 5) -> str:
messages = [{"role": "user", "content": user_message}]
for _ in range(max_steps):
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
tools=TOOLS,
messages=messages,
)
if response.stop_reason != "tool_use":
return response.content[0].text
messages.append({"role": "assistant", "content": response.content})
tool_results = []
for block in response.content:
if block.type != "tool_use":
continue
func = TOOL_FUNCTIONS[block.name]
try:
result = func(**block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result,
})
except Exception as e:
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": str(e),
"is_error": True,
})
messages.append({"role": "user", "content": tool_results})
return "Reached max steps without a final answer."
if __name__ == "__main__":
print(run_agent("What time is it, and how much is AAPL worth in EUR right now?"))
A single call here can trigger Claude to request get_current_time, get_stock_price, and get_exchange_rate — possibly in one parallel batch — then combine all three results into a final answer.
Summary
- A tool is
name+description+input_schema(JSON Schema) - Tool use is a loop: call → check
stop_reason == "tool_use"→ execute everytool_useblock → sendtool_resultblocks → repeat - Claude can request multiple tools in one turn — match by
tool_use_id, not order -
tool_choice:auto,any, a specifictool, ornone— forcing a tool is a clean way to get structured output -
get_final_message()gives parsedtool_useblocks even when streaming - Set
is_error: trueon failed tool results - Write clear descriptions, use enums, keep required fields minimal, cap agent loops with a step limit
Further reading:
Top comments (0)