VOOZH about

URL: https://dev.to/kalyna_pro/claude-api-function-calling-complete-guide-to-tool-use-2026-1kh3

⇱ Claude API Function Calling: Complete Guide to Tool Use (2026) - DEV Community


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:

  1. Call messages.create() with tools and the conversation so far
  2. If stop_reason != "tool_use", Claude is done — return its text
  3. Otherwise, execute every tool_use block in response.content and collect the results
  4. Append Claude's response and a new user message containing tool_result blocks, 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 enum to constrain free-text values
  • Keep required minimal
  • 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 every tool_use block → send tool_result blocks → repeat
  • Claude can request multiple tools in one turn — match by tool_use_id, not order
  • tool_choice: auto, any, a specific tool, or none — forcing a tool is a clean way to get structured output
  • get_final_message() gives parsed tool_use blocks even when streaming
  • Set is_error: true on failed tool results
  • Write clear descriptions, use enums, keep required fields minimal, cap agent loops with a step limit

Further reading: