Use the beta_tool Decorator

Su Suriya Ravichandran Updated 10 Nov 2026
9 min read ·Lesson 91 of 200

Less Boilerplate for Tools

In Lessons 81–90, each tool needed a hand-written definition: a name, a description, and a JSON Schema, plus a separate Python function, plus code to connect the two. The SDK offers a shortcut: the @beta_tool decorator turns an ordinary, type-hinted Python function into a tool.

from anthropic import beta_tool


@beta_tool
def get_weather(city: str) -> str:
    """Get the current weather for a city.

    Args:
        city: City name, e.g. Chennai
    """
    return "31°C, humid, light rain expected at 5 PM"

According to the SDK documentation, the decorator inspects the function's arguments and docstring to derive the JSON Schema for you. One function now provides everything:

Tool partComes from
Tool nameThe function name (get_weather)
Tool descriptionThe docstring's description text
Parameters and their typesThe function's parameters and type hints
Required vs. optionalParameters without defaults are required; parameters with defaults are optional
Parameter descriptionsThe Args: section of the docstring
What runs when Claude calls the toolThe function itself

Decorated tools are designed for the SDK's tool runner (Lesson 92), which runs the tool loop for you. Both are beta features, which is why they live under the beta names.


How the Schema Is Derived

Type Hints Become Schema Types

Python type hintJSON Schema type
strstring
intinteger
floatnumber
boolboolean
list[str]array of strings
Literal["a", "b"]an enumeration of allowed values

Type hints are therefore not optional decoration here: they define what Claude must send. A parameter without a type hint gives Claude no type information.

Defaults Make Parameters Optional

@beta_tool
def search_courses(topic: str, level: str = "any") -> str:
    ...

topic has no default, so it is required. level has a default, so it is optional: Claude may omit it, and your function then receives "any".

The Docstring Becomes the Description

The decorator reads a Google-style docstring: a description, followed by an Args: section with one line per parameter:

"""Search the GOCOURSE catalog for courses on a topic.

Use it when a student asks which courses exist or wants a recommendation.
Do not use it for the student's own enrollments.

Args:
    topic: Subject to search for, e.g. 'Django' or 'SQL'
    level: Difficulty filter: 'beginner', 'intermediate', 'advanced', or 'any'
"""

Everything you learned about descriptions applies (Lesson 82): explain what the tool does, when to use it, when not to, and what each parameter means. Lesson 93 covers docstrings in depth.


Return Values

Tools return their result as a string, which becomes the tool result's text. According to the documentation, to return structured data, such as an object or a number, encode it as a string first, typically as JSON:

import json


@beta_tool
def get_enrollments() -> str:
    """Return the current student's enrolled courses with progress percentages, as JSON."""
    return json.dumps([{"course": "django-101", "progress": 65}])

A tool can also return content blocks, such as images, for multimodal results.

Errors

If a decorated tool raises an exception, the tool runner catches it and sends Claude a tool result with is_error: true. In Python, that result contains the exception's type and message, not the stack trace, and the SDK logs the full exception with Python's logging module. Write exception messages that help Claude recover (Lesson 90):

raise ValueError("No weather data for 'Mumbai'. Supported cities: Chennai, Bengaluru.")

Async Tools

With the async client (AsyncAnthropic, Lessons 71–80), use @beta_async_tool and define the function with async def:

from anthropic import beta_async_tool


@beta_async_tool
async def get_weather(city: str) -> str:
    """Get the current weather for a city.

    Args:
        city: City name, e.g. Chennai
    """
    return await fetch_weather(city)   # Your async implementation

Inspecting the Generated Definition

You can see exactly what the decorator generated, which is useful for checking descriptions and types before Claude ever sees them:

import json

print(json.dumps(get_weather.to_dict(), indent=2))

Always inspect generated definitions while developing. Mistakes in docstrings, such as a missing Args: entry or a typo in a parameter name, show up immediately in the output.


Practical Example: Decorated Study-Assistant Tools

This example defines three tools with @beta_tool, prints their generated definitions, and runs a question through the SDK's tool runner (introduced fully in Lesson 92).

"""decorated_tools.py: define tools with @beta_tool and run them with the tool runner."""

import json
from typing import Literal

from anthropic import Anthropic, beta_tool

WEATHER = {"chennai": "31°C, humid, light rain expected at 5 PM",
           "bengaluru": "24°C, partly cloudy, no rain expected"}
COURSES = [
    {"slug": "django-101", "title": "Django for Beginners", "level": "beginner", "weeks": 6},
    {"slug": "django-apis", "title": "Advanced Django APIs", "level": "advanced", "weeks": 4},
    {"slug": "sql-basics", "title": "SQL Basics", "level": "beginner", "weeks": 3},
]


@beta_tool
def get_weather(city: str) -> str:
    """Get the current weather for a city.

    Use it for questions about current conditions or whether the weather suits an activity.
    Returns temperature in Celsius and a short description. It does not provide forecasts.

    Args:
        city: City name, e.g. Chennai
    """
    weather = WEATHER.get(city.strip().lower())
    if weather is None:
        raise ValueError(f"No weather data for {city!r}. Supported cities: Chennai, Bengaluru.")
    return weather


@beta_tool
def search_courses(topic: str, level: Literal["beginner", "intermediate", "advanced", "any"] = "any") -> str:
    """Search the GOCOURSE catalog for courses on a topic.

    Use it when a student asks which courses exist or wants a recommendation.
    Returns up to 5 courses as JSON with slug, title, level, and duration in weeks.

    Args:
        topic: Subject to search for, e.g. 'Django' or 'SQL'
        level: Difficulty filter; use 'any' to include all levels
    """
    matches = [c for c in COURSES
               if topic.lower() in c["slug"] and (level == "any" or c["level"] == level)]
    return json.dumps(matches[:5]) if matches else f"No courses found for {topic!r}."


@beta_tool
def get_course_details(slug: str) -> str:
    """Get details for one course by its slug.

    Use it after search_courses when the student wants more information about a specific course.

    Args:
        slug: Course slug from search_courses, e.g. 'django-101'
    """
    for course in COURSES:
        if course["slug"] == slug:
            return json.dumps(course)
    raise ValueError(f"No course with slug {slug!r}. Use search_courses to find valid slugs.")


if __name__ == "__main__":
    print(json.dumps(search_courses.to_dict(), indent=2))

    client = Anthropic()
    runner = client.beta.messages.tool_runner(
        model="claude-opus-5-5",
        max_tokens=4096,
        tools=[get_weather, search_courses, get_course_details],
        messages=[{"role": "user", "content":
                   "Which beginner Django course do you have, how long is it, "
                   "and is it a nice day for a walk in Bengaluru?"}],
    )
    final = runner.until_done()
    print("".join(block.text for block in final.content if block.type == "text"))

Example output (the schema's exact formatting may differ between SDK versions):

{
  "name": "search_courses",
  "description": "Search the GOCOURSE catalog for courses on a topic.\n\nUse it when a student asks ...",
  "input_schema": {
    "type": "object",
    "properties": {
      "topic": {"type": "string", "description": "Subject to search for, e.g. 'Django' or 'SQL'"},
      "level": {"enum": ["beginner", "intermediate", "advanced", "any"], "type": "string",
                "default": "any", "description": "Difficulty filter; use 'any' to include all levels"}
    },
    "required": ["topic"]
  }
}
Django for Beginners is our beginner Django course; it takes 6 weeks. Bengaluru is 24°C and partly cloudy with no rain expected, so it's a nice day for a walk.

How It Works

  1. Each @beta_tool function becomes a tool object. The function name becomes the tool name, the docstring's description becomes the tool description, and the Args: entries become parameter descriptions.
  2. Type hints define the schema. topic: str with no default is required. level uses Literal[...], so Claude can only choose one of the listed values, and its default "any" makes it optional.
  3. Return values are strings. Structured results are returned as JSON with json.dumps(). An empty search returns a clear message instead of an empty list.
  4. Failures raise exceptions with helpful messages. The tool runner converts them into error results for Claude (Lesson 90).
  5. to_dict() shows the generated definition, so you can check exactly what Claude will see.
  6. client.beta.messages.tool_runner(...) receives the decorated functions directly in tools. until_done() runs the whole loop (calling tools, returning results, and continuing) and returns Claude's final message. Lesson 92 explains the runner in detail.

Compare this with Lessons 86–88: the same behavior needed a registry, input models, a run_tool() function, and a hand-written loop. The decorator and runner handle those parts for you.


When to Use the Decorator

Use @beta_tool and the runner when…Use manual definitions and a manual loop when…
Tools are ordinary Python functionsTool definitions come from elsewhere, such as configuration or another service
You want the standard loop behaviorYou need full control: approval steps, custom result formatting, special error handling (Lessons 86–90)
You value less code and fewer places for mistakesYou need features of the loop that the runner does not expose

The documentation itself recommends the manual loop when you need human-in-the-loop approval, custom logging, or conditional execution. Both approaches use the same underlying API, so you can start with the decorator and switch later if needed.


Common Mistakes

Missing Type Hints

What happens: the schema lacks type information, and Claude has to guess what to send.

Fix: type-hint every parameter, using Literal for fixed choices.

Docstrings Without an Args: Section

What happens: parameters have no descriptions.

Fix: write a Google-style docstring with one Args: entry per parameter.

Returning Non-String Values

What happens: structured data is not encoded as the tool result expects.

Fix: return strings; use json.dumps() for structured data.

Vague Exception Messages

What happens: Claude sees an error such as KeyError: 'x' and cannot recover.

Fix: raise exceptions with messages that explain the problem and the next step.

Using @beta_tool with the Async Client

What happens: synchronous tools block the event loop in async applications.

Fix: use @beta_async_tool with async def functions.


Best Practices

  • Use @beta_tool for tools implemented as Python functions.
  • Type-hint every parameter, and use defaults for optional ones.
  • Write complete docstrings: what, when, when not, returns, and every parameter.
  • Return strings, with JSON for structured data.
  • Raise clear exceptions for failures that Claude should know about.
  • Inspect to_dict() output during development.
  • Switch to a manual loop when you need custom control.

Exercise

  1. Add a get_enrollments() tool with no parameters that returns a JSON list of courses and progress. Print its to_dict() output.
  2. Remove the Args: section from get_course_details and print its definition again. What changed?
  3. Ask the runner about the weather in Mumbai. How does Claude respond to the exception message?
  4. Rewrite get_weather as an async tool with @beta_async_tool.
Show answers for steps 2 and 4

Step 2: the slug parameter is still required and still has the type string, from the type hint, but it no longer has a description. Claude can still call the tool, but it has less guidance about what a valid slug looks like, which makes mistakes more likely.

Step 4:

from anthropic import beta_async_tool


@beta_async_tool
async def get_weather(city: str) -> str:
    """Get the current weather for a city.

    Use it for questions about current conditions or whether the weather suits an activity.
    Returns temperature in Celsius and a short description. It does not provide forecasts.

    Args:
        city: City name, e.g. Chennai
    """
    weather = WEATHER.get(city.strip().lower())
    if weather is None:
        raise ValueError(f"No weather data for {city!r}. Supported cities: Chennai, Bengaluru.")
    return weather

Use it with AsyncAnthropic and the async tool runner (Lesson 92). In a real application, the function body would await an async HTTP call to a weather service (Lesson 98).

0 Comments

Reviewed before they appear

No comments yet.

Anthropic SDK Using Python
Anthropic SDK Intro Claude API Intro SDK Requirements Install Anthropic SDK Upgrade the SDK Create an API Key Set API Keys Use Environment Variables Import Anthropic Create a Client Send Your First Message Choose a Claude Model Set Max Tokens Read a Response Print Response Text Understand Message Blocks Use System Prompts Send User Messages Set Temperature Handle Stop Reasons Messages API Intro Create Messages Use Message Roles Send Conversation History Continue a Conversation Build a Chat Loop Reset Chat History Use Multiple Turns Inspect Message IDs Read Usage Data Write Clear Prompts Set a System Prompt Add Context to Prompts Use Prompt Templates Add Examples to Prompts Request JSON Output Use XML Tags Control Response Length Improve Prompt Quality Avoid Prompt Injection Read Text Blocks Read Content Blocks Check Stop Reasons Count Input Tokens Count Output Tokens Count Tokens Before Sending Limit Output Tokens Use Stop Sequences Read Request IDs Parse API Responses Streaming Intro Stream Text Output Use Stream Events Use Stream Helpers Get Final Messages Stream Async Responses Handle Stream Errors Build a Live Chat Stream Long Responses Close Streams Safely Build a Chatbot Keep Chat History Manage Chat Sessions Add Chat Memory Build a CLI Chatbot Build a Web Chatbot Use Flask with Claude Use FastAPI with Claude Return JSON from an API Add Chat Error Messages Async SDK Intro Create an Async Client Send Async Messages Use asyncio Run Parallel Requests Stream Async Text Handle Async Errors Set Async Timeouts Close Async Clients Choose Sync or Async Tool Use Intro Define a Tool Create Tool Schemas Send Tools to Claude Read Tool Calls Run Python Tools Return Tool Results Continue After Tool Use Control Tool Choice Handle Tool Errors Use the beta_tool Decorator Create a Tool Runner Add Tool Docstrings Validate Tool Inputs Use Multiple Tools Limit Tool Calls Log Tool Activity Build a Weather Tool Build a Search Tool Build a Calculator Tool Structured Output Intro Request JSON Data Validate JSON Output Use Pydantic Models Handle Invalid JSON Extract Fields from Text Generate Typed Data Use Image URLs Send Base64 Images Read PDFs with Claude Use Output Schemas Build Data Extraction Test Structured Responses Send Image Inputs Upload Files Use File IDs Process Text Files Summarize Documents Extract Table Data Handle File Errors Read Cache Usage Reduce Repeated Input Costs Debug Cache Misses Prompt Caching Intro Enable Prompt Caching Cache System Prompts Cache Long Context Test Cache Performance Use Cache Breakpoints Cache Best Practices Message Batches Intro Create a Message Batch Set Batch IDs Check Batch Status List Message Batches Get Batch Results Process Batch Outputs Handle Failed Batch Items Use Batches for Bulk Tasks Compare Batches and Requests Handle API Errors Handle Rate Limits Retry Failed Requests Set Request Timeouts Handle Network Errors Use Exponential Backoff Build Reliable API Calls Use Extended Thinking Log API Errors Track Request IDs Paginate API Results Set Thinking Budgets Read Thinking Blocks Use Tool Search Use Web Search Tools Use Computer Tools Use Citations Use Token Counting Read Raw Responses Set Custom Headers Use Claude on AWS Bedrock Use Claude on Vertex AI Use Claude on Microsoft Foundry Configure Cloud Credentials Choose a Cloud Provider Compare API Integrations Configure Provider Clients Handle Cloud Errors Use Cloud SDK Extras Deploy Cloud Apps Protect API Keys Use a .env File Ignore Secrets in Git Validate User Input Reduce Prompt Injection Limit Tool Permissions Avoid Unsafe Tool Calls Sanitize Tool Results Set Usage Limits Review Security Logs Test SDK Functions Mock API Responses Test Error Handling Test Streaming Code Measure Token Usage Track API Costs Configure Logging Deploy with Docker Deploy a FastAPI App Monitor Production Apps Build a Claude Chatbot Build a Text Summarizer Build a Code Explainer Build a JSON Extractor Build a PDF Assistant Build a Streaming Chat App Build a Tool-Calling Agent Build a Batch Processor Build a Research Assistant Build a Claude API Dashboard
Ask about this post
AI Ask about this post

Ask questions about Use the beta_tool Decorator and get answers drawn from it.

Signed-in readers only.