Add Tool Docstrings
Docstrings Are Now Prompts
With @beta_tool (Lesson 91), a function's docstring becomes the tool description that Claude reads, and the docstring's Args: section becomes the parameter descriptions. A docstring is no longer just documentation for developers. It is part of the prompt, and the most important factor in whether Claude uses the tool correctly (Lesson 82).
@beta_tool
def get_course_details(slug: str) -> str:
"""Get details for one course by its slug. ← tool description (what)
Use it after search_courses when ... ← when to use it
Do not use it to find courses; ... ← when not to use it
Returns the course as JSON with ... ← what it returns
Args:
slug: Course slug from search_courses, ... ← parameter description
"""
The Google Docstring Format
The decorator reads Google-style docstrings:
"""Summary line.
Longer description: one or more paragraphs.
Args:
first_param: Description of the first parameter.
second_param: Description of the second parameter. Descriptions
can continue on an indented line.
"""
| Part | Becomes |
|---|---|
| Summary line and longer description | The tool's description |
Each Args: entry | The description of the matching parameter in input_schema |
The parameter name in Args: must match the function parameter exactly. A typo, such as slugs: for a parameter named slug, leaves the real parameter without a description.
What a Good Tool Docstring Contains
Apply the guidance from Lesson 82 to the docstring: aim for at least 3–4 sentences that cover:
| Content | Example |
|---|---|
| What the tool does | "Get details for one course by its slug." |
| When to use it | "Use it after search_courses when the student wants more information about a specific course." |
| When not to use it | "Do not use it to find courses; use search_courses instead." |
| What it returns | "Returns the course as JSON with title, level, duration in weeks, and a short summary." |
| Limits | "It does not return prices or schedules." |
And for each parameter:
| Content | Example |
|---|---|
| Meaning | "Course slug from search_courses" |
| Format and example | "lowercase with hyphens, e.g. 'django-101'" |
| Allowed values | "one of 'beginner', 'intermediate', 'advanced'" (or use Literal in the type hint) |
| Behavior when omitted | "defaults to 'any', which includes all levels" |
A Template
"""<What the tool does, in one sentence>.
Use it when <situations>. Do not use it for <situations>; use <other tool> instead.
Returns <format and fields>. <Limits: what it does not return or do>.
Args:
<param>: <meaning>, <format>, e.g. <example>. <behavior when omitted, if optional>.
"""
What Not to Put in Tool Docstrings
Because docstrings are sent to Claude, everything in them becomes part of the prompt:
| Do not include | Why | Put it here instead |
|---|---|---|
| Internal notes ("TODO: fix pagination", "hack for old DB") | Confuses the model, and wastes tokens | # comments in the code |
| Secrets, internal hostnames, credentials | Can be revealed in conversation | Configuration and environment variables (Lesson 8) |
Implementation details ("queries table courses_v2 with a JOIN") | Irrelevant to how Claude should use the tool | Code comments or developer docs |
| Instructions that contradict your system prompt | Creates conflicting guidance | One consistent policy in the system prompt |
Use code comments for developer-facing notes, and the docstring for model-facing guidance.
Docstrings for Tools Without Parameters
A tool with no parameters still needs a full description. Explain what it returns and why no input is needed:
@beta_tool
def get_enrollments() -> str:
"""Return the current student's enrolled courses with progress percentages, as JSON.
Use it for questions about the student's own courses, progress, or what to study next.
It takes no input: the student is identified automatically by the application.
"""
The last sentence prevents Claude from looking for a way to pass a student ID, and makes clear that identity comes from the application (Lesson 86).
Practical Example: A Docstring Checker for Decorated Tools
This checker reads the definition that @beta_tool generates, through to_dict(), and reports common docstring problems. Running it in tests catches weak or broken docstrings before they reach Claude.
"""docstring_check.py: check the descriptions that @beta_tool generates from docstrings."""
import json
import re
from anthropic import beta_tool
def check_tool(tool) -> list[str]:
definition = tool.to_dict()
problems = []
description = definition.get("description", "")
sentences = [s for s in re.split(r"[.!?](?:\s|$)", description) if s.strip()]
if len(sentences) < 3:
problems.append(f"description has {len(sentences)} sentence(s); aim for at least 3")
properties = definition.get("input_schema", {}).get("properties", {})
for name, schema in properties.items():
if not schema.get("description"):
problems.append(f"parameter {name!r} has no description")
return problems
@beta_tool
def get_course(slug: str) -> str:
"""Get course."""
return json.dumps({"slug": slug})
@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.
Do not use it to find courses; use search_courses instead.
Returns the course as JSON with title, level, and duration in weeks.
Args:
slug: Course slug from search_courses, lowercase with hyphens, e.g. 'django-101'
"""
# Internal note (not sent to Claude): data comes from the catalog cache.
return json.dumps({"slug": slug, "title": "Django for Beginners", "level": "beginner", "weeks": 6})
if __name__ == "__main__":
for tool, label in [(get_course, "get_course"), (get_course_details, "get_course_details")]:
print(f"{label}: {check_tool(tool) or 'OK'}")
Output:
get_course: ['description has 1 sentence(s); aim for at least 3', "parameter 'slug' has no description"]
get_course_details: OK
How It Works
tool.to_dict()returns the definition generated from the function: name, description, and input schema (Lesson 91). The checker tests what Claude will actually see, not the source code.- Sentence counting splits the description on
.,!, or?followed by whitespace or the end of the text. It is a rough measure, but it reliably flags one-line descriptions. - Parameter checks look at each property in
input_schema. A property without a description means the docstring is missing anArgs:entry, or has a typo in a parameter name. get_coursefails both checks: its docstring is a two-word summary, with noArgs:section.get_course_detailspasses. Its internal note is a#comment inside the function body, which is not part of the docstring and is never sent to Claude.
Add the checker to your test suite: assert check_tool(tool) == [] for every tool.
Testing Whether Descriptions Work
A complete docstring is necessary, but you should also check that it works: that Claude chooses the right tool with the right inputs. Use the evaluation approach from Lesson 39:
- Write test questions that should, and should not, trigger each tool.
- Run them, and record which tools Claude calls and with what inputs.
- When Claude chooses wrongly, improve the relevant "when to use" or "when not to use" sentences, and run the tests again.
Common Mistakes
One-Line Docstrings
What happens: Claude has too little information to choose and use the tool correctly.
Fix: write 3–4 sentences or more: what, when, when not, returns, limits.
Missing or Misspelled Args: Entries
What happens: parameters lose their descriptions without any error.
Fix: match every parameter name exactly, and check the generated definition.
Internal Notes in Docstrings
What happens: TODOs, implementation details, or secrets are sent to Claude.
Fix: keep developer notes in # comments.
Docstrings That Conflict with the System Prompt
What happens: Claude receives contradictory instructions about when to use tools.
Fix: keep tool-specific guidance in docstrings, and overall policy in the system prompt.
Never Checking the Generated Definition
What happens: formatting mistakes go unnoticed.
Fix: inspect to_dict() output, and check it in tests.
Best Practices
- Treat tool docstrings as prompts.
- Use Google-style docstrings with an
Args:entry for every parameter. - Describe what, when, when not, returns, and limits.
- Include formats and examples in parameter descriptions.
- Keep internal notes in comments, never in docstrings.
- Check generated definitions automatically, and evaluate tool choice with test questions.
Exercise
- Rewrite the docstring of
get_courseso that it passes the checker. - Add a check to
check_tool()that warns when the description is longer than 1,200 characters. - Write a docstring for a
cancel_enrollment(slug: str, reason: str)tool that makes clear when it should not be used. - Explain why the internal note in
get_course_detailsis a comment and not part of the docstring.
Step 3:
@beta_tool
def cancel_enrollment(slug: str, reason: str) -> str:
"""Cancel the current student's enrollment in one course.
Use it only when the student explicitly asks to cancel or leave a specific course and has
confirmed the course. Do not use it when a student is only unhappy, asking about refunds,
or asking what would happen if they cancelled; answer those questions instead.
Returns a confirmation with the cancellation date, or an error if the student is not enrolled.
Args:
slug: Slug of the course to cancel, e.g. 'django-101'
reason: The student's reason for cancelling, in their own words, in one sentence
"""
Actions that change data deserve especially precise "when not to use" guidance. In production, such tools should also require explicit confirmation before running (Lesson 177).
Step 4: everything in a docstring becomes part of the tool description sent to Claude. The note is useful only to developers. Sending it would waste tokens, could confuse the model, and could reveal implementation details. A # comment inside the function body is ignored by the decorator, so it stays private to the code.