Use the beta_tool Decorator
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 part | Comes from |
|---|---|
| Tool name | The function name (get_weather) |
| Tool description | The docstring's description text |
| Parameters and their types | The function's parameters and type hints |
| Required vs. optional | Parameters without defaults are required; parameters with defaults are optional |
| Parameter descriptions | The Args: section of the docstring |
| What runs when Claude calls the tool | The 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 hint | JSON Schema type |
|---|---|
str | string |
int | integer |
float | number |
bool | boolean |
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
- Each
@beta_toolfunction becomes a tool object. The function name becomes the tool name, the docstring's description becomes the tool description, and theArgs:entries become parameter descriptions. - Type hints define the schema.
topic: strwith no default is required.levelusesLiteral[...], so Claude can only choose one of the listed values, and its default"any"makes it optional. - 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. - Failures raise exceptions with helpful messages. The tool runner converts them into error results for Claude (Lesson 90).
to_dict()shows the generated definition, so you can check exactly what Claude will see.client.beta.messages.tool_runner(...)receives the decorated functions directly intools.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 functions | Tool definitions come from elsewhere, such as configuration or another service |
| You want the standard loop behavior | You need full control: approval steps, custom result formatting, special error handling (Lessons 86–90) |
| You value less code and fewer places for mistakes | You 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_toolfor 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
- Add a
get_enrollments()tool with no parameters that returns a JSON list of courses and progress. Print itsto_dict()output. - Remove the
Args:section fromget_course_detailsand print its definition again. What changed? - Ask the runner about the weather in Mumbai. How does Claude respond to the exception message?
- Rewrite
get_weatheras an async tool with@beta_async_tool.
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).