DEV Community

Cover image for Claude API Function Calling: Complete Guide to Tool Use (2026)
Serhii Kalyna
Serhii Kalyna

Posted on • Originally published at kalyna.pro

Claude API Function Calling: Complete Guide to Tool Use (2026)

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-..."
Enter fullscreen mode Exit fullscreen mode

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"],
        },
    }
]
Enter fullscreen mode Exit fullscreen mode

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})
Enter fullscreen mode Exit fullscreen mode

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"],
    },
})
Enter fullscreen mode Exit fullscreen mode

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'}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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,
    })
Enter fullscreen mode Exit fullscreen mode

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?"))
Enter fullscreen mode Exit fullscreen mode

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:

Top comments (0)