Interactive Calculator CLI
This is not a toy calculator.
This project is designed to force you to build a small, reliable system that lives at Python’s most important boundary:
Human input.
You will practice:
- Robust input and output
- Type casting and coercion rules
- Operator semantics and precedence
- Truthiness and validation patterns
- Defensive engineering (no crashes on bad input)
- Safe evaluation without using eval
By the end, you will have a CLI tool that behaves like a real product.
Concept Overview
You are building a loop-driven CLI application that:
1) reads user commands
2) validates and parses them
3) executes operations safely
4) prints results in a predictable format
5) handles errors without crashing
This is exactly what production CLIs do.
Non-Negotiable Rule
You are not allowed to use:
eval(...)
Because:
- It executes arbitrary code
- It is unsafe
- It teaches the wrong habits
- It bypasses real parsing and operator reasoning
You will build a safe evaluator instead.
What You Will Build
Your calculator will support:
- Basic operations: add, sub, mul, div
- Extended operations: floor division, modulo, power
- Unary operations: negation, absolute
- Memory register: store and recall a value
- History: list last N computations
- Exit commands: exit, quit
Input examples:
add 10 20
sub 100 45
mul 7 8
div 10 4
floordiv 10 4
mod 10 4
pow 2 10
abs -42
neg 15
Engineering Outcomes (Why This Project Exists)
After this project, you should be able to explain:
- Why input is always a string and must be parsed
- Why casting can fail and must be defended
- Why division returns float and floor division returns int
- Why truthiness can break validation if done incorrectly
- Why operators are not just symbols but runtime mechanics
- Why safe evaluation matters in production
Project Phases
You will build this in steps.
Do not skip steps.
Each step exists to teach a runtime concept.
Step 1 - Skeleton CLI Loop (No Math Yet)
Goal: Create a CLI that continuously reads commands until user exits.
Requirements:
- Print a banner on start
- Show a prompt each loop
- Exit on "exit" or "quit"
- Ignore empty input safely
Implementation outline:
def main():
print("Calculator CLI. Type 'help' for commands. Type 'exit' to quit.")
while True:
raw = input("calc> ").strip()
if not raw:
continue
if raw in ("exit", "quit"):
print("Bye.")
break
Test Cases:
- Input empty line should not crash
- "exit" should stop
- "quit" should stop
Reflection:
- Why does input return str?
- Why should you strip whitespace?
Step 2 - Tokenization and Command Parsing
Goal: Convert input text into tokens.
Example:
Input:
add 10 20
Tokens:
["add", "10", "20"]
Rules:
- Split on whitespace
- First token is command
- Rest are arguments
Implementation:
tokens = raw.split()
cmd = tokens[0].lower()
args = tokens[1:]
Test Cases:
- Extra spaces between words
- Mixed case commands
Reflection:
- Why should commands be lowercased?
- What happens if user types only "add"?
Step 3 - Type Conversion Layer (The Parser)
Goal: Convert args to numbers safely.
Your parser should support:
- int
- float
Rules:
- Try int first if it looks like integer
- Otherwise try float
- Reject invalid values
Implementation idea:
def parse_number(token: str):
try:
if token.isdigit() or (token.startswith("-") and token[1:].isdigit()):
return int(token)
return float(token)
except ValueError:
raise ValueError(f"Invalid number: {token}")
Test Cases:
- "10"
- "-10"
- "3.14"
- "-0.5"
- "abc" should error safely
Reflection:
- Why does int("10.5") fail?
- Why is float precision dangerous?
Step 4 - Operator Execution Layer
Goal: Create a dispatch table from command name to function.
Example mapping:
OPS = {
"add": lambda a, b: a + b,
"sub": lambda a, b: a - b,
"mul": lambda a, b: a * b,
"div": lambda a, b: a / b,
}
Then:
- Validate correct number of args
- Parse args
- Execute
- Print result
Core execution:
func = OPS[cmd]
a = parse_number(args[0])
b = parse_number(args[1])
result = func(a, b)
print(result)
Test Cases:
- div 10 4 returns float
- div 10 0 handled safely
Reflection:
- Why does / always return float?
- Why does division by zero raise error?
Step 5 - Error Handling as Product Behavior
Goal: Never crash on bad input.
All errors should:
- Show a clean error message
- Continue loop
Pattern:
try:
...
except Exception as e:
print(f"Error: {e}")
continue
But do not hide bugs silently.
For production:
- catch expected errors
- let unexpected errors surface in development
Test Cases:
- Unknown command
- Missing args
- Invalid numbers
Reflection:
- Why is swallowing all exceptions dangerous?
- Which exceptions should be handled?
Step 6 - Add Help Command
Implement:
help
Should show:
- All supported commands
- Usage patterns
- Examples
Make help output readable and consistent.
Step 7 - Add Memory Register
Add:
- store (stores last result)
- recall (prints stored value)
- clear (clears memory)
Design:
- memory starts as None
- recall errors if memory is None
Example:
add 5 5
store
recall
Reflection:
- Why is None best sentinel here?
- Why should you check identity using is None?
Step 8 - Add History
Store last N calculations.
Requirements:
- history command prints latest entries
- store expression and result
- limit length to N (like 20)
Data model:
history = []
history.append(("add 1 2", 3))
Reflection:
- Why should history store raw string and parsed numbers?
- What is the trade-off?
Safe Evaluation Challenge (Optional Advanced)
Support expressions like:
2 + 3 * 4
Without eval.
Approach:
- Tokenize numbers and operators
- Implement precedence:
- power
- multiply and divide
- add and subtract
- Evaluate safely
This is a small expression parser.
Not required, but extremely valuable.
Output Format Standard
Results should be printed as:
- integers as integers
- floats with limited precision
Example:
result: 2
result: 2.5
Avoid printing huge float artifacts.
Use rounding when necessary.
Engineering Checklist
Your final calculator must:
- Never crash on bad input
- Never use eval
- Validate arg counts per command
- Handle division by zero cleanly
- Support help, history, memory
- Be readable and modular
- Keep parsing separate from execution
Stretch Extensions
- Add config for history size
- Add support for parentheses in expression parsing
- Add scientific functions: sqrt, sin, cos
- Add unit tests for parser and ops
- Add a "mode" for int-only operations
Reflection Questions
- Where did type casting cause bugs during development?
- Which validation rules saved you from crashes?
- Which operators produced surprising type outputs?
- How did truthiness affect your input validation?
- Why is building a safe evaluator a security lesson?
Your Motto Check
If you can explain every surprising behavior, you are learning Python.
If you cannot explain it, the project is working.
