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
You need the anthropic Python SDK and an API key. If you don’t have a key yet, see How to Get a Claude API Key.
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 — there is no hidden documentation it can fall back on, so write the description like you’re onboarding a new teammate.
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"],
},
}
]Pass tools alongside messages in client.messages.create(). If Claude decides the tool is needed, the response will have stop_reason == "tool_use" and response.content will include a tool_use block with name, input (the arguments, already parsed as a dict), and a unique id you’ll need to send the result back.
The Tool-Use Loop
Tool use is a loop, not a single call. Each iteration follows the same four steps:
- 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 go back to step 1
from anthropic import Anthropic
client = Anthropic()
def get_stock_price(ticker: str, currency: str = "USD") -> str:
# Replace with a real market data API call
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})Notice the loop already iterates over every block in response.content and collects results into a single tool_result message. That detail matters for the next section.
Parallel Tool Calls
When you provide multiple tools, Claude can request several of them in a single response — for example, looking up two stocks at once, or fetching a price and an exchange rate together. In that case response.content contains more than one tool_use block, each with its own id.
Add a second tool:
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"],
},
})With both tools available, a prompt like “What’s Apple’s stock price in EUR?” can make Claude return two tool_use blocks in one turn — one for get_stock_price, one for get_exchange_rate. The loop from the previous section already handles this correctly because it iterates over every block and replies with one tool_result per tool_use_id. The order of the results in the message doesn’t matter — Claude matches each tool_result to its tool_use block by tool_use_id, not by position.
Controlling Tool Choice
The tool_choice parameter controls whether and how Claude must use tools:
{"type": "auto"}— default. Claude decides whether a tool is needed.{"type": "any"}— Claude must call one of the provided tools, but you don’t pick which.{"type": "tool", "name": "..."}— Claude must call this specific tool.{"type": "none"}— Claude must not call any tool, even iftoolsis provided.
Forcing a specific tool is a clean way to get structured JSON output — define a tool that represents the shape you want, force Claude to call it, and read the arguments straight from response.content[0].input:
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 [email protected], phone +1 555-1234.",
}],
)
contact = response.content[0].input
print(contact)
# {'name': 'John Doe', 'email': '[email protected]', 'phone': '+1 555-1234'}This avoids prompting Claude to “respond in JSON” and parsing its text output — the arguments are already a dict, validated against your schema. You can also pass "disable_parallel_tool_use": True inside tool_choice to guarantee exactly one tool call back.
Streaming Tool Inputs
Tool arguments can be streamed too, which matters for large inputs or when you want to show progress before the call completes. The SDK’s streaming helper emits input_json_delta events containing fragments of the arguments JSON as Claude generates it:
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()
# final.content[block_index].input is the fully parsed dict —
# no need to assemble the partial_json fragments yourself
for block in final.content:
if block.type == "tool_use":
print(block.name, block.input)In practice you rarely need to parse partial_json manually — get_final_message() gives you fully-parsed tool_use blocks once the stream ends, with the same shape as a non-streaming response. Streaming is mainly useful here to render a “Claude is preparing a tool call…” indicator in a UI.
Error Handling in Tool Results
If a tool raises an exception or returns an error, tell Claude by setting is_error: true on the tool_result block instead of silently swallowing the exception:
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 text and adapts — it might retry with different arguments, try a different tool, or explain the failure to the user. Without is_error, Claude may treat a stack trace or error string as a valid result and build an answer on top of it.
Best Practices for Tool Definitions
- Write descriptions like API docs for a new teammate — name, units, format, and edge cases all matter
- Use
enumto constrain free-text values instead of describing valid options in prose - Keep
requiredminimal — every required field is a chance for Claude to omit it and trigger a validation error - One tool = one job. Avoid “do everything” mega-tools with a dozen optional parameters
- Return concise, structured text or JSON from tools — large blobs count as input tokens on every following call
- Name tools with verbs (
get_,search_,create_) so their purpose is unambiguous at a glance
Complete Example: A Multi-Tool Agent
Putting it all together — three tools, a dispatch table, and a loop with a step limit so a misbehaving model can’t run forever:
from anthropic import Anthropic
from datetime import datetime, timezone
client = Anthropic()
# --- Tool implementations ---------------------------------------------------
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,
}
# --- Tool schemas ------------------------------------------------------------
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": {}},
},
]
# --- Agent loop ---------------------------------------------------------------
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 to run_agent() here triggers 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 natural-language answer. The max_steps guard is cheap insurance: without it, a tool that always returns an error could loop indefinitely.
Summary
- A tool is
name+description+input_schema(JSON Schema) — Claude has no other context about it - Tool use is a loop: call → check
stop_reason == "tool_use"→ execute everytool_useblock → send backtool_resultblocks → repeat - Claude can request multiple tools in one turn (parallel tool use) — match results to calls via
tool_use_id, not order tool_choicecontrols behavior:auto,any, a specifictool, ornone— forcing a specific tool is a clean way to get structured output- Streaming exposes
input_json_deltaevents, butget_final_message()already gives you parsedtool_useblocks - Set
is_error: trueon failed tool results so Claude can adapt instead of treating an error string as data - Write tool descriptions like documentation, use enums, keep required fields minimal, and cap agent loops with a step limit
Further reading: How to Build an AI Agent with Python for a broader agent architecture, and Building MCP Servers for Claude — MCP is built on the same tool-use foundation covered here.
Subscribe to my newsletter — practical guides on Claude API, AI agents, RAG, and automation.