How to Implement Tool Use and Function Calling in the Anthropic Python SDK

The Anthropic Python SDK enables Claude to call external functions by decorating Python callables with @beta_tool, passing the resulting BetaFunctionTool objects to client.messages.create(tools=[...]), and processing the returned tool_use blocks either manually or via the automatic tool_runner utility.

The anthropics/anthropic-sdk-python repository provides native support for function calling through its beta tooling module, allowing developers to expose Python functions as AI-callable tools. This implementation leverages Pydantic v2 for automatic JSON schema generation and offers both synchronous and asynchronous execution patterns. Below is a comprehensive guide to implementing tool use and function calling based on the actual SDK source code.

Defining Tools with the @beta_tool Decorator

The foundation of tool use begins with the @beta_tool decorator defined in [src/anthropic/lib/tools/_beta_functions.py](https://github.com/anthropics/anthropic-sdk-python/blob/main/src/anthropic/lib/tools/_beta_functions.py). When applied to a Python function, this decorator returns a BetaFunctionTool instance that automatically infers the tool's JSON schema from type hints and extracts descriptions from docstrings.

The decorator performs three critical operations during initialization:

  • Schema inference: The _create_schema_from_function method (lines 28-71) uses Pydantic v2's TypeAdapter to generate the input_schema from function type annotations
  • Description extraction: The _get_description_from_docstring helper (lines 19-26) parses the function docstring to populate the tool description
  • Validation setup: The resulting object provides a call(input: object) method that validates incoming arguments against the generated schema before invoking the original function
from typing_extensions import Literal
from anthropic import beta_tool
import json

@beta_tool
def get_weather(location: str, units: Literal["c", "f"]) -> str:
    """Lookup the weather for a city.
    
    Args:
        location: City name (e.g. "San Francisco, CA").
        units: Desired temperature units, "c" for Celsius or "f" for Fahrenheit.
    """
    return json.dumps({
        "location": location,
        "temperature": "20°C" if units == "c" else "68°F",
        "condition": "Sunny"
    })

Decorated functions expose a to_dict() method that serializes the tool into the BetaToolParam format expected by the API, including the name, description, and input_schema fields.

Sending Tool Definitions to the API

Once defined, tools are passed to the Messages API via the tools parameter in client.messages.create(). The implementation in [src/anthropic/resources/messages/messages.py](https://github.com/anthropics/anthropic-sdk-python/blob/main/src/anthropic/resources/messages/messages.py) (lines 48-66) accepts an Iterable[ToolUnionParam], which includes both built-in tool types and the generic ToolParam produced by BetaFunctionTool.

During request construction, the SDK's maybe_transform utility (lines 1000-1016) automatically converts BetaFunctionTool instances to their dictionary representation via to_dict(). The resulting payload structure sent to Claude resembles:

{
  "type": "tool",
  "name": "get_weather",
  "description": "Lookup the weather for a city.",
  "input_schema": {
    "type": "object",
    "properties": {
      "location": {"type": "string"},
      "units": {"enum": ["c", "f"], "type": "string"}
    },
    "required": ["location", "units"]
  }
}

Manually defining tools without the decorator is also supported by constructing ToolParam dictionaries directly, though this requires manually specifying the JSON schema:

from anthropic.types import ToolParam

tools: list[ToolParam] = [
    {
        "name": "get_weather",
        "description": "Get the weather for a specific location",
        "input_schema": {
            "type": "object",
            "properties": {
                "location": {"type": "string"},
                "units": {"enum": ["c", "f"], "type": "string"}
            },
            "required": ["location", "units"]
        }
    }
]

message = client.messages.create(
    model="claude-3-5-sonnet-latest",
    max_tokens=1024,
    tools=tools,
    messages=[{"role": "user", "content": "What is the weather in SF?"}]
)

Handling tool_use Blocks Manually

When Claude decides to invoke a tool, the response contains a tool_use content block. The SDK parses this into a ToolUseBlock model defined in [src/anthropic/types/tool_use_block.py](https://github.com/anthropics/anthropic-sdk-python/blob/main/src/anthropic/types/tool_use_block.py), which includes:

  • id: A unique identifier (e.g., toolu_01D...) for correlating results
  • name: The registered tool name matching the function definition
  • input: A dictionary conforming to the declared input_schema

To handle this manually, iterate through the message content to locate the block, execute the corresponding Python function, and return a tool_result message:

import json
from anthropic.types import MessageParam

# Initial request

user_msg: MessageParam = {"role": "user", "content": "What is the weather in SF?"}
msg = client.messages.create(
    model="claude-3-5-sonnet-latest",
    max_tokens=1024,
    messages=[user_msg],
    tools=[get_weather]  # List of BetaFunctionTool objects

)

# Check for tool_use stop reason

if msg.stop_reason == "tool_use":
    tool_use = next(c for c in msg.content if c.type == "tool_use")
    
    # Execute the function with validated arguments

    result = get_weather(**tool_use.input)
    
    # Send tool_result back to Claude

    final_msg = client.messages.create(
        model="claude-3-5-sonnet-latest",
        max_tokens=1024,
        messages=[
            user_msg,
            {"role": msg.role, "content": msg.content},
            {
                "role": "user",
                "content": [{
                    "type": "tool_result",
                    "tool_use_id": tool_use.id,
                    "content": [{"type": "text", "text": result}]
                }]
            }
        ]
    )
    print(final_msg.content)

This manual flow provides full control over error handling, logging, and result formatting, as demonstrated in the official [examples/tools.py](https://github.com/anthropics/anthropic-sdk-python/blob/main/examples/tools.py).

Automatic Execution with the Beta Tool Runner

For streamlined implementations, the SDK provides client.beta.messages.tool_runner, an abstraction that automates the request-response loop. This utility, showcased in [examples/tools_runner.py](https://github.com/anthropics/anthropic-sdk-python/blob/main/examples/tools_runner.py), handles tool detection, execution, and result submission automatically.

The runner operates as an iterator, yielding each assistant message in the conversation flow:

from anthropic import Anthropic, beta_tool

client = Anthropic()

@beta_tool
def get_weather(location: str, units: Literal["c", "f"]) -> str:
    """Lookup weather for a location."""
    return json.dumps({
        "location": location,
        "temperature": "20°C" if units == "c" else "68°F",
        "condition": "Sunny"
    })

runner = client.beta.messages.tool_runner(
    model="claude-3-5-sonnet-latest",
    max_tokens=1024,
    tools=[get_weather],
    messages=[{"role": "user", "content": "What is the weather in San Francisco?"}]
)

for message in runner:
    print(message.content)

Under the hood, the runner:

  1. Sends the initial request with tool definitions
  2. Detects tool_use blocks in assistant responses
  3. Locates the matching BetaFunctionTool by name and validates inputs
  4. Invokes tool.call(input) and packages the return value into a tool_result message
  5. Resubmits the conversation history, allowing Claude to continue with the function result

Asynchronous Tool Implementation

For I/O-bound operations, the SDK supports asynchronous functions via the @beta_async_tool decorator (lines 93-121 in _beta_functions.py). The runner automatically detects async functions and awaits their execution:

from anthropic import beta_async_tool
import asyncio

@beta_async_tool
async def async_get_weather(location: str, units: Literal["c", "f"]) -> str:
    """Async weather lookup suitable for external HTTP calls."""
    # Simulate async I/O

    await asyncio.sleep(0.1)
    return json.dumps({
        "location": location,
        "temperature": "68°F",
        "condition": "Sunny"
    })

runner = client.beta.messages.tool_runner(
    model="claude-3-5-sonnet-latest",
    max_tokens=1024,
    tools=[async_get_weather],
    messages=[{"role": "user", "content": "What's the weather in New York?"}]
)

# Iterate normally—the runner handles awaiting internally

for msg in runner:
    print(msg.content)

Summary

  • Tool definition: Apply @beta_tool or @beta_async_tool to Python functions to generate JSON schemas automatically via Pydantic v2, as implemented in _beta_functions.py
  • API integration: Pass decorated functions to client.messages.create(tools=[...]) or use the tool_runner utility; the Messages resource handles serialization in messages.py
  • Manual handling: Inspect response content for ToolUseBlock objects, execute functions with tool_use.input, and return tool_result blocks with the original tool_use_id
  • Automatic execution: Use client.beta.messages.tool_runner to handle the entire request-execute-respond loop without manual state management
  • Async support: The beta runner detects coroutines automatically and awaits them, enabling non-blocking I/O operations for external API calls

Frequently Asked Questions

How does the @beta_tool decorator generate JSON schemas automatically?

The decorator invokes BaseFunctionTool._create_schema_from_function (lines 28-71 in _beta_functions.py), which uses Pydantic v2's TypeAdapter and ValidateCall to inspect function type hints and build the JSON schema. It also extracts the description from the function docstring using _get_description_from_docstring, requiring no manual schema writing for standard Python functions.

What is the difference between manual tool handling and the beta tool runner?

Manual handling requires the developer to check message.stop_reason, locate ToolUseBlock instances in the content array, call the Python function directly, and construct the tool_result message structure manually. The beta tool runner (client.beta.messages.tool_runner) automates this entire workflow, managing the conversation state and automatically invoking registered functions when Claude emits tool_use blocks.

Can I mix synchronous and asynchronous tools in the same conversation?

No, a single tool runner instance must use either all synchronous or all asynchronous tools. However, you can create separate runner instances for different conversation flows. If using manual handling, you control the execution context and can mix sync and async functions by awaiting async tools explicitly while calling synchronous tools normally.

What should I do if my tool function raises an exception?

When using the beta runner, exceptions are automatically caught and formatted as error content blocks in the tool_result message sent back to Claude. For manual implementations, wrap the function call in a try-except block and return the error message as text content within the tool_result block, ensuring you maintain the tool_use_id correlation for proper conversation continuity.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →