Build a Calculator Tool

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

Why Give Claude a Calculator

Claude is good at reasoning about numbers, but exact arithmetic, especially with many digits, several steps, or functions such as square roots, is exactly the kind of work that deterministic code does more reliably. A calculator tool lets Claude decide what to calculate, while your code computes the exact result.

TaskWithout a toolWith a calculator tool
Price after a 10% discount and 18% GSTUsually right, occasionally offExact every time
Compound interest over 7 yearsError-prone multi-step arithmeticOne expression, exact result
Unit conversions with long decimalsRounding mistakes possibleExact, with controlled rounding

This lesson also teaches an important security lesson: never evaluate model-generated code with eval().


Why Not eval()?

The obvious implementation is dangerous:

@beta_tool
def calculate(expression: str) -> str:
    return str(eval(expression))      # DANGEROUS: never do this

eval() runs any Python expression. An expression such as __import__('os').system('rm -rf ~') would run a shell command on your server. The input comes from the model, and the model's input can be influenced by users and by content it reads, for example through prompt injection (Lesson 40). A tool that executes arbitrary code hands that power to anyone who can steer the conversation.

Warning: Do not pass model-generated text to eval(), exec(), shell commands, or SQL queries. Parse it, allow only what you explicitly support, and reject everything else.


The Safe Approach: Parse, Then Evaluate a Whitelist

Python's ast module parses an expression into an abstract syntax tree (AST): a tree of nodes that describes the structure of the code without running it:

import ast

tree = ast.parse("4999 * 0.9 * 1.18", mode="eval")
print(ast.dump(tree.body))
# BinOp(left=BinOp(left=Constant(value=4999), op=Mult(), right=Constant(value=0.9)),
#       op=Mult(), right=Constant(value=1.18))

A safe evaluator walks this tree and handles only whitelisted node types:

AllowedExamples
Numbers (Constant with int or float)4999, 0.9
Binary operations+ - * / // % **
Unary operations-5, +5
Named constantspi, e
Calls to whitelisted functionssqrt(2), round(x, 2)

Anything else, including attribute access, imports, strings, lists, comprehensions, and unknown functions, raises an error before anything runs.

Resource Limits

Even pure arithmetic can be abused: 9 ** 9 ** 9 would try to compute a number with hundreds of millions of digits, consuming CPU and memory. The evaluator therefore limits:

  • the length of the expression (200 characters), and
  • the size of exponents (at most 100).

Practical Example: A Safe Calculator Tool

"""calculator_tool.py: a safe calculator tool built on Python's ast module."""

import ast
import math
import operator

from anthropic import Anthropic, beta_tool

MAX_EXPRESSION_CHARS = 200
MAX_EXPONENT = 100

BINARY = {ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul,
          ast.Div: operator.truediv, ast.FloorDiv: operator.floordiv,
          ast.Mod: operator.mod, ast.Pow: operator.pow}
UNARY = {ast.UAdd: operator.pos, ast.USub: operator.neg}
FUNCTIONS = {"sqrt": math.sqrt, "log": math.log, "log10": math.log10, "exp": math.exp,
             "sin": math.sin, "cos": math.cos, "tan": math.tan,
             "abs": abs, "round": round, "floor": math.floor, "ceil": math.ceil}
CONSTANTS = {"pi": math.pi, "e": math.e}


class CalculationError(ValueError):
    """A calculation problem, with a message written for Claude."""


def evaluate(node: ast.AST):
    if isinstance(node, ast.Expression):
        return evaluate(node.body)
    if isinstance(node, ast.Constant) and type(node.value) in (int, float):
        return node.value
    if isinstance(node, ast.Name) and node.id in CONSTANTS:
        return CONSTANTS[node.id]
    if isinstance(node, ast.BinOp) and type(node.op) in BINARY:
        left, right = evaluate(node.left), evaluate(node.right)
        if isinstance(node.op, ast.Pow) and abs(right) > MAX_EXPONENT:
            raise CalculationError(f"Exponent {right} is too large (maximum {MAX_EXPONENT}).")
        return BINARY[type(node.op)](left, right)
    if isinstance(node, ast.UnaryOp) and type(node.op) in UNARY:
        return UNARY[type(node.op)](evaluate(node.operand))
    if isinstance(node, ast.Call):
        name = node.func.id if isinstance(node.func, ast.Name) else ast.unparse(node.func)
        if name not in FUNCTIONS or node.keywords:
            raise CalculationError(f"Unknown function {name!r}. Available functions: {', '.join(FUNCTIONS)}.")
        return FUNCTIONS[name](*[evaluate(arg) for arg in node.args])
    if isinstance(node, ast.Tuple):
        raise CalculationError("Commas are only allowed between function arguments. "
                               "Remove thousands separators, e.g. write 4999 instead of 4,999.")
    raise CalculationError(f"Unsupported element: {type(node).__name__}. Use numbers, + - * / // % **, "
                           "parentheses, pi, e, and the available functions.")


def calculate_expression(expression: str):
    text = expression.strip().replace("^", "**")
    if len(text) > MAX_EXPRESSION_CHARS:
        raise CalculationError(f"The expression is too long (maximum {MAX_EXPRESSION_CHARS} characters).")
    try:
        tree = ast.parse(text, mode="eval")
    except SyntaxError:
        raise CalculationError(f"Could not parse {expression!r}. Write one arithmetic expression, "
                               "e.g. '4999 * 0.9 * 1.18'.") from None
    try:
        return evaluate(tree)
    except CalculationError:
        raise
    except ZeroDivisionError:
        raise CalculationError("Division by zero.") from None
    except (ValueError, OverflowError, TypeError) as error:
        raise CalculationError(f"Math error: {error}.") from None


def format_number(value) -> str:
    if isinstance(value, float):
        if value.is_integer() and abs(value) < 1e15:
            return str(int(value))
        return format(value, ".10g")
    return str(value)


@beta_tool
def calculate(expression: str) -> str:
    """Evaluate an arithmetic expression exactly and return the result.

    Use it for any arithmetic that matters: prices, discounts, taxes, percentages, interest,
    unit conversions, or multi-step calculations. Do not do such arithmetic in your head.
    Supports + - * / // % ** (or ^), parentheses, the constants pi and e, and the functions
    sqrt, log, log10, exp, sin, cos, tan, abs, round, floor, and ceil.
    Returns the expression and its result, with up to 10 significant digits.

    Args:
        expression: One arithmetic expression without thousands separators, e.g. '4999 * 0.9 * 1.18'
    """
    return f"{expression} = {format_number(calculate_expression(expression))}"


if __name__ == "__main__":
    tests = ["4999 * 0.9 * 1.18", "2^10", "sqrt(2) * 3", "round(1234.5678, 2)",
             "10 / 0", "2 ** 1000", "4,999 * 2", "open('secrets.txt')"]
    for expression in tests:
        try:
            print(f"{expression} -> {format_number(calculate_expression(expression))}")
        except CalculationError as error:
            print(f"{expression} -> ERROR: {error}")

    runner = Anthropic().beta.messages.tool_runner(
        model="claude-opus-5-5",
        max_tokens=4096,
        max_iterations=5,
        tools=[calculate],
        messages=[{"role": "user", "content":
                   "A course costs ₹4,999. Students get 10% off, and then 18% GST is added. "
                   "What do students pay, rounded to the nearest rupee?"}],
    )
    final = runner.until_done()
    print("\n" + "".join(b.text for b in final.content if b.type == "text"))

Output (the final answer's wording varies):

4999 * 0.9 * 1.18 -> 5308.938
2^10 -> 1024
sqrt(2) * 3 -> 4.242640687
round(1234.5678, 2) -> 1234.57
10 / 0 -> ERROR: Division by zero.
2 ** 1000 -> ERROR: Exponent 1000 is too large (maximum 100).
4,999 * 2 -> ERROR: Commas are only allowed between function arguments. Remove thousands separators, e.g. write 4999 instead of 4,999.
open('secrets.txt') -> ERROR: Unknown function 'open'. Available functions: sqrt, log, log10, exp, sin, cos, tan, abs, round, floor, ceil.

Students pay ₹5,309. The 10% discount brings the price to ₹4,499.10, and adding 18% GST gives ₹5,308.94, which rounds to ₹5,309.

How It Works

  1. ast.parse(text, mode="eval") turns the text into a syntax tree without running it. mode="eval" accepts only a single expression, not statements such as imports or assignments. Invalid syntax becomes a helpful CalculationError.
  2. evaluate() walks the tree recursively and handles only whitelisted node types. Numbers must be exactly int or float: type(...) in (int, float) also rejects True and False, which Python treats as integers. Binary and unary operators are looked up in dictionaries that map AST operator classes to functions from the operator module.
  3. Function calls are allowed only for names in FUNCTIONS, and without keyword arguments. open('secrets.txt') is rejected before anything is called, with a message listing the available functions.
  4. ast.Tuple appears when the expression contains a stray comma, such as 4,999, which Python parses as the tuple (4, 999 * 2). A specific message tells Claude to remove thousands separators.
  5. Resource limits reject long expressions and large exponents. Because evaluate() computes both sides of ** before applying it, 2 ** 1000 is caught by checking the right side's value.
  6. calculate_expression() converts ^ to **, since ^ is a common way to write powers but means bitwise XOR in Python, and converts Python's own errors into CalculationError messages: division by zero, math domain errors such as sqrt(-1), overflows, and wrong argument counts.
  7. format_number() shows whole-number floats as integers, and other floats with up to 10 significant digits. 4999 * 0.9 * 1.18 prints as 5308.938, hiding tiny binary floating-point artifacts.
  8. The tool's docstring tells Claude to use it for any arithmetic that matters, and to avoid mental arithmetic. In the run, Claude calculated the discounted, taxed price with the tool and rounded the exact result.

For larger computations, such as data analysis, statistics, or charts, Anthropic's code execution server tool runs code in a secure sandbox on Anthropic's servers, so you never execute model-generated code on your own machines (Lesson 155).


Common Mistakes

Using eval() or exec() on Model Output

What happens: remote code execution. Anyone who can influence the conversation can run code on your server.

Fix: parse with ast and evaluate a strict whitelist.

Blacklisting Instead of Whitelisting

What happens: forbidding a few dangerous names, such as __import__, always misses some other way to cause harm.

Fix: allow only explicitly supported node types and functions; reject everything else.

No Resource Limits

What happens: expressions such as 9 ** 9 ** 9 consume CPU and memory for a very long time.

Fix: limit expression length and exponent size.

Confusing Error Messages

What happens: Claude receives Python's raw SyntaxError text and repeats the same mistake.

Fix: explain the problem and show a valid example.

Telling Claude the Tool Exists but Not When to Use It

What happens: Claude still does arithmetic itself, without the tool.

Fix: say in the docstring to use it for all arithmetic that matters.


Best Practices

  • Never execute model-generated code directly.
  • Parse and whitelist: allow only the operations and functions you support.
  • Limit resource use with length and size limits.
  • Translate errors into helpful messages with examples.
  • Format results consistently, without floating-point noise.
  • Describe when to use the tool, so Claude relies on it for exact arithmetic.
  • Use the code execution server tool for heavier computations.

Exercise

  1. Add min and max to FUNCTIONS, and test max(3, 7, 5).
  2. Add a percent_of(part, whole) function that returns part / whole * 100, and describe it in the docstring.
  3. Try sqrt(-1), log(0), and sqrt(1, 2). What messages do you get?
  4. Explain why type(node.value) in (int, float) is used instead of isinstance(node.value, (int, float)).
Show answers for steps 3 and 4

Step 3: all three raise Python errors inside evaluate(), which calculate_expression() converts into CalculationError messages that begin with Math error:. sqrt(-1) and log(0) raise ValueError for math domain problems, and sqrt(1, 2) raises a TypeError about the wrong number of arguments. The exact wording after Math error: comes from Python and depends on your Python version: older versions say math domain error, while newer versions give more specific descriptions. Either way, Claude receives a clear signal that the expression itself is invalid.

Step 4: in Python, bool is a subclass of int, so isinstance(True, int) is True. With isinstance, the expression True + True would be accepted and return 2, which is technically harmless, but not arithmetic the tool means to support. type(...) in (int, float) accepts only actual integers and floats, which keeps the whitelist exact.

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 Build a Calculator Tool and get answers drawn from it.

Signed-in readers only.