PythonMastery
Python programs · Useful programs · intermediate

Text-based calculator in Python

A calculator that reads lines like 7 * 6 and prints the answer, handles bad input and division by zero, and never uses eval().

This calculator reads a line such as 7 * 6, works out the answer and asks for the next line, until you type q. It checks every line and gives a clear message when the line is wrong.

The program

python
import operator

OPERATIONS = {
    "+": operator.add,
    "-": operator.sub,
    "*": operator.mul,
    "/": operator.truediv,
    "**": operator.pow,
}


def calculate(line):
    parts = line.split()
    if len(parts) != 3:
        return "Type a number, an operator and a number, like 7 * 6"
    left, symbol, right = parts
    if symbol not in OPERATIONS:
        return f"Unknown operator {symbol!r}. Use one of: + - * / **"
    try:
        a = float(left)
        b = float(right)
    except ValueError:
        return "Both sides must be numbers"
    if symbol == "/" and b == 0:
        return "Cannot divide by zero"
    try:
        result = OPERATIONS[symbol](a, b)
    except OverflowError:
        return "That answer is too large to show"
    except ZeroDivisionError:
        return "Cannot divide by zero"
    return f"{result:g}"


while True:
    line = input("calc> ")
    if line.strip() in ("q", "quit"):
        break
    print(calculate(line))

Typed in this example: 12 + 30, 7 * 6, 10 / 4, 5 / 0, 2 ^ 8, ten + 1, 2 ** 10, 10 ** 400, 0 ** -1, q. Press Run to type your own.

output
calc> 12 + 30
42
calc> 7 * 6
42
calc> 10 / 4
2.5
calc> 5 / 0
Cannot divide by zero
calc> 2 ^ 8
Unknown operator '^'. Use one of: + - * / **
calc> ten + 1
Both sides must be numbers
calc> 2 ** 10
1024
calc> 10 ** 400
That answer is too large to show
calc> 0 ** -1
Cannot divide by zero
calc> q

How it works

Why not eval()?

Many calculator examples pass the whole line to eval(). That runs the text as Python code, so a user could type __import__('os').remove('notes.txt') and delete a file. Never call eval() on text that a user typed.

Another way: full expressions

To accept expressions like (2 + 3) * 4, with brackets and the normal order of operations, you can let Python's own parser read the text and then allow only numbers and the four operators. The ast module gives you the parsed expression as a tree:

python
import ast
import operator

ALLOWED = {
    ast.Add: operator.add,
    ast.Sub: operator.sub,
    ast.Mult: operator.mul,
    ast.Div: operator.truediv,
    ast.USub: operator.neg,
}


def evaluate(node):
    if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
        return node.value
    if isinstance(node, ast.BinOp) and type(node.op) in ALLOWED:
        return ALLOWED[type(node.op)](evaluate(node.left), evaluate(node.right))
    if isinstance(node, ast.UnaryOp) and type(node.op) in ALLOWED:
        return ALLOWED[type(node.op)](evaluate(node.operand))
    raise ValueError("only numbers and + - * / are allowed")


def calculate(text):
    return evaluate(ast.parse(text, mode="eval").body)


for expression in ["2 + 3 * 4", "(2 + 3) * 4", "100 / 8", "-4 * -2.5"]:
    print(expression, "=", calculate(expression))

try:
    calculate("__import__('os').getcwd()")
except ValueError as error:
    print("Rejected:", error)
output
2 + 3 * 4 = 14
(2 + 3) * 4 = 20
100 / 8 = 12.5
-4 * -2.5 = 10.0
Rejected: only numbers and + - * / are allowed

evaluate() calls itself for the left and right side of each operator. Anything that is not a number or an allowed operator, such as a function call, raises a ValueError. The power operator is left out on purpose: 9 ** 9 ** 9 would keep the computer busy for a very long time.

Learn the idea: Functions & Scoping, Errors & Exceptions: When Things Go Wrong, Dictionaries

A longer explanation: Project: CLI Calculator (with History)