Build a fully-featured command-line calculator in Python step-by-step. Covers basic arithmetic, advanced functions, input validation, error handling, history logging, and a clean OOP-based implementation.
In this project, you'll build a fully featured command-line calculator from scratch — starting with basic operations and progressively adding features like history, scientific functions, and OOP design.
Project Overview
What you'll build: A multi-function calculator with a clean menu interface
Skills practiced: Functions, loops, error handling, classes, input validation
Estimated time: 2–3 hours
Version 1: Basic Calculator
# calculator_v1.py — Simple four-function calculator
def add(a, b):
return a + b
def subtract(a, b):
return a - b
def multiply(a, b):
return a * b
def divide(a, b):
if b == 0:
raise ZeroDivisionError("Cannot divide by zero!")
return a / b
def get_number(prompt):
"""Safely get a number from the user."""
while True:
try:
return float(input(prompt))
except ValueError:
print("❌ Please enter a valid number.")
def calculator_v1():
print("=" * 40)
print(" Simple Python Calculator")
print("=" * 40)
while True:
print("\nOperations:")
print(" 1. Add (+)")
print(" 2. Subtract (-)")
print(" 3. Multiply (*)")
print(" 4. Divide (/)")
print(" 5. Quit")
choice = input("\nChoose operation (1-5): ").strip()
if choice == '5':
print("👋 Goodbye!")
break
if choice not in ['1', '2', '3', '4']:
print("❌ Invalid choice. Please choose 1-5.")
continue
a = get_number("Enter first number: ")
b = get_number("Enter second number: ")
try:
if choice == '1':
result = add(a, b)
op = '+'
elif choice == '2':
result = subtract(a, b)
op = '-'
elif choice == '3':
result = multiply(a, b)
op = '×'
elif choice == '4':
result = divide(a, b)
op = '÷'
print(f"\n✅ {a} {op} {b} = {result}")
except ZeroDivisionError as e:
print(f"❌ Error: {e}")
if __name__ == '__main__':
calculator_v1()
Version 2: Enhanced with More Operations
# calculator_v2.py — Enhanced calculator with more operations
import math
def format_number(num):
"""Format number nicely — int if whole, float otherwise."""
if isinstance(num, complex):
return str(num)
if num == int(num):
return str(int(num))
return f"{num:.10g}" # Remove trailing zeros
OPERATIONS = {
'1': ('Add', '+', lambda a, b: a + b),
'2': ('Subtract', '-', lambda a, b: a - b),
'3': ('Multiply', '×', lambda a, b: a * b),
'4': ('Divide', '÷', lambda a, b: a / b),
'5': ('Power', '^', lambda a, b: a ** b),
'6': ('Modulus', '%', lambda a, b: a % b),
'7': ('Floor Divide', '//', lambda a, b: a // b),
}
UNARY_OPERATIONS = {
'8': ('Square Root', 'sqrt', math.sqrt),
'9': ('Factorial', 'n!', lambda a: math.factorial(int(a))),
'10': ('Logarithm (base 10)', 'log10', math.log10),
'11': ('Natural Log', 'ln', math.log),
'12': ('Sine', 'sin', lambda a: math.sin(math.radians(a))),
'13': ('Cosine', 'cos', lambda a: math.cos(math.radians(a))),
'14': ('Tangent', 'tan', lambda a: math.tan(math.radians(a))),
'15': ('Absolute Value', 'abs', abs),
}
def get_number(prompt="Enter number: "):
while True:
try:
return float(input(prompt))
except ValueError:
print("❌ Invalid input. Enter a number.")
def calculator_v2():
print("=" * 50)
print(" Enhanced Python Calculator v2.0")
print("=" * 50)
history = []
while True:
print("\n--- BINARY OPERATIONS ---")
for key, (name, symbol, _) in OPERATIONS.items():
print(f" {key:2}. {name} ({symbol})")
print("\n--- UNARY OPERATIONS (one number) ---")
for key, (name, symbol, _) in UNARY_OPERATIONS.items():
print(f" {key:2}. {name} ({symbol})")
print("\n H. History")
print(" Q. Quit")
choice = input("\nChoose: ").strip().upper()
if choice == 'Q':
print("👋 Goodbye!")
break
if choice == 'H':
if not history:
print("No history yet.")
else:
print("\n📋 Calculation History:")
for i, entry in enumerate(history[-10:], 1):
print(f" {i}. {entry}")
continue
try:
if choice in OPERATIONS:
name, symbol, func = OPERATIONS[choice]
a = get_number("First number: ")
b = get_number("Second number: ")
result = func(a, b)
expr = f"{format_number(a)} {symbol} {format_number(b)} = {format_number(result)}"
elif choice in UNARY_OPERATIONS:
name, symbol, func = UNARY_OPERATIONS[choice]
a = get_number("Enter number: ")
result = func(a)
expr = f"{symbol}({format_number(a)}) = {format_number(result)}"
else:
print("❌ Invalid choice.")
continue
print(f"\n✅ {expr}")
history.append(expr)
except ZeroDivisionError:
print("❌ Error: Cannot divide by zero!")
except ValueError as e:
print(f"❌ Math Error: {e}")
except OverflowError:
print("❌ Error: Number too large!")
if __name__ == '__main__':
calculator_v2()
Version 3: Object-Oriented Calculator
# calculator_v3.py — OOP-based calculator with memory and persistent history
import math
import json
import os
from datetime import datetime
class CalculatorError(Exception):
"""Custom exception for calculator errors."""
pass
class Calculator:
"""Feature-rich OOP calculator with memory and history."""
CONSTANTS = {
'pi': math.pi,
'e': math.e,
'phi': (1 + math.sqrt(5)) / 2, # Golden ratio
'inf': math.inf,
}
def __init__(self, history_file='calc_history.json'):
self.memory = 0.0
self.last_result = 0.0
self.history = []
self.history_file = history_file
self._load_history()
# ─── Basic Operations ──────────────────────────────────────
def add(self, a, b): return a + b
def subtract(self, a, b): return a - b
def multiply(self, a, b): return a * b
def divide(self, a, b):
if b == 0:
raise CalculatorError("Division by zero is undefined")
return a / b
def power(self, a, b): return a ** b
def modulus(self, a, b):
if b == 0:
raise CalculatorError("Modulus by zero is undefined")
return a % b
def floor_divide(self, a, b):
if b == 0:
raise CalculatorError("Division by zero is undefined")
return a // b
# ─── Scientific Operations ─────────────────────────────────
def sqrt(self, a):
if a < 0:
raise CalculatorError("Cannot take square root of negative number")
return math.sqrt(a)
def factorial(self, a):
if a < 0 or a != int(a):
raise CalculatorError("Factorial requires a non-negative integer")
if a > 170:
raise CalculatorError("Number too large for factorial")
return math.factorial(int(a))
def log(self, a, base=10):
if a <= 0:
raise CalculatorError("Logarithm requires a positive number")
if base == math.e:
return math.log(a)
return math.log(a, base)
def sin_deg(self, a): return math.sin(math.radians(a))
def cos_deg(self, a): return math.cos(math.radians(a))
def tan_deg(self, a):
if abs(math.cos(math.radians(a))) < 1e-10:
raise CalculatorError("Tangent is undefined for this angle")
return math.tan(math.radians(a))
def nth_root(self, a, n):
if n == 0:
raise CalculatorError("Root degree cannot be zero")
return a ** (1/n)
# ─── Memory Operations ─────────────────────────────────────
def memory_store(self, value):
self.memory = value
print(f" 💾 Memory stored: {self._fmt(value)}")
def memory_recall(self):
print(f" 📤 Memory: {self._fmt(self.memory)}")
return self.memory
def memory_add(self, value):
self.memory += value
print(f" 💾 Memory updated: {self._fmt(self.memory)}")
def memory_clear(self):
self.memory = 0.0
print(" 🗑️ Memory cleared")
# ─── History ───────────────────────────────────────────────
def _add_to_history(self, expression, result):
entry = {
'expression': expression,
'result': result,
'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
}
self.history.append(entry)
self.last_result = result
self._save_history()
def show_history(self, n=10):
if not self.history:
print(" No history yet.")
return
print(f"\n 📋 Last {min(n, len(self.history))} calculations:")
for i, entry in enumerate(self.history[-n:], 1):
print(f" {i:2}. {entry['expression']} = {self._fmt(entry['result'])}")
print(f" [{entry['timestamp']}]")
def clear_history(self):
self.history = []
self._save_history()
print(" 🗑️ History cleared")
def _save_history(self):
try:
with open(self.history_file, 'w') as f:
json.dump(self.history[-100:], f, indent=2) # Keep last 100
except IOError:
pass
def _load_history(self):
try:
if os.path.exists(self.history_file):
with open(self.history_file) as f:
self.history = json.load(f)
print(f" 📂 Loaded {len(self.history)} history entries")
except (IOError, json.JSONDecodeError):
self.history = []
# ─── Utilities ─────────────────────────────────────────────
def _fmt(self, num):
"""Format number for display."""
if isinstance(num, complex):
return str(num)
if isinstance(num, int):
return f"{num:,}"
if abs(num) > 1e12 or (abs(num) < 1e-6 and num != 0):
return f"{num:.6e}"
if num == int(num):
return f"{int(num):,}"
return f"{num:.10g}"
def get_input(self, prompt="Enter number: "):
"""Get number input with support for ANS (last answer) and constants."""
while True:
raw = input(prompt).strip().lower()
if raw == 'ans':
return self.last_result
if raw in self.CONSTANTS:
return self.CONSTANTS[raw]
try:
return float(raw)
except ValueError:
print(f" ❌ Invalid input. Use a number, 'ans', or {list(self.CONSTANTS.keys())}")
def run(self):
"""Main calculator loop."""
print("\n" + "=" * 55)
print(" Advanced Python Calculator v3.0")
print(" Type 'ans' to use last result, or constant names")
print(" Available constants: pi, e, phi, inf")
print("=" * 55)
while True:
print("\n ── BINARY ──────────────────────────────")
print(" 1. Add 2. Subtract 3. Multiply 4. Divide")
print(" 5. Power 6. Modulus 7. Floor Div 8. Nth Root")
print("\n ── SCIENTIFIC ──────────────────────────")
print(" 9. √x 10. x! 11. log10 12. ln")
print(" 13. sin 14. cos 15. tan")
print("\n ── MEMORY ──────────────────────────────")
print(" MS. Store MR. Recall M+. Add MC. Clear")
print("\n ── HISTORY ─────────────────────────────")
print(" H. History CH. Clear History")
print(" Q. Quit")
choice = input("\n > ").strip().upper()
try:
if choice == 'Q':
self._save_history()
print(" 👋 Goodbye!")
break
elif choice == 'H':
self.show_history()
elif choice == 'CH':
self.clear_history()
elif choice in ('MS', 'MR', 'M+', 'MC'):
if choice == 'MS':
val = self.get_input(" Value to store: ")
self.memory_store(val)
elif choice == 'MR':
self.memory_recall()
elif choice == 'M+':
val = self.get_input(" Value to add to memory: ")
self.memory_add(val)
elif choice == 'MC':
self.memory_clear()
# Binary operations
elif choice in map(str, range(1, 9)):
a = self.get_input(" First number: ")
b = self.get_input(" Second number: ")
ops = {
'1': (self.add, '+'),
'2': (self.subtract, '-'),
'3': (self.multiply, '×'),
'4': (self.divide, '÷'),
'5': (self.power, '^'),
'6': (self.modulus, '%'),
'7': (self.floor_divide, '//'),
'8': (self.nth_root, 'root'),
}
func, sym = ops[choice]
result = func(a, b)
expr = f"{self._fmt(a)} {sym} {self._fmt(b)}"
print(f"\n ✅ {expr} = {self._fmt(result)}")
self._add_to_history(expr, result)
# Unary operations
elif choice in map(str, range(9, 16)):
a = self.get_input(" Number: ")
ops = {
'9': (self.sqrt, 'sqrt'),
'10': (self.factorial, '!'),
'11': (lambda x: self.log(x, 10), 'log10'),
'12': (lambda x: self.log(x, math.e), 'ln'),
'13': (self.sin_deg, 'sin'),
'14': (self.cos_deg, 'cos'),
'15': (self.tan_deg, 'tan'),
}
func, sym = ops[choice]
result = func(a)
expr = f"{sym}({self._fmt(a)})"
print(f"\n ✅ {expr} = {self._fmt(result)}")
self._add_to_history(expr, result)
else:
print(" ❌ Invalid choice.")
except CalculatorError as e:
print(f"\n ❌ Calculator Error: {e}")
except Exception as e:
print(f"\n ❌ Unexpected Error: {e}")
if __name__ == '__main__':
calc = Calculator()
calc.run()
Version 4: GUI Calculator (tkinter)
# calculator_gui.py — Simple tkinter GUI calculator
import tkinter as tk
from tkinter import messagebox
import math
class CalculatorGUI:
def __init__(self, root):
self.root = root
self.root.title("Python Calculator")
self.root.resizable(False, False)
self.root.configure(bg='#1e1e1e')
self.expression = ""
self.display_var = tk.StringVar(value="0")
self._build_ui()
def _build_ui(self):
# Display
display_frame = tk.Frame(self.root, bg='#1e1e1e', padx=10, pady=10)
display_frame.pack(fill='x')
display = tk.Label(display_frame, textvariable=self.display_var,
font=('Arial', 28, 'bold'), bg='#333', fg='white',
anchor='e', padx=15, pady=10, width=14)
display.pack(fill='x')
# Button layout
buttons = [
['C', '±', '%', '÷'],
['7', '8', '9', '×'],
['4', '5', '6', '-'],
['1', '2', '3', '+'],
['0', '.', '=', ''],
]
btn_colors = {
'C': '#FF3B30', '±': '#555', '%': '#555', '÷': '#FF9500',
'×': '#FF9500', '-': '#FF9500', '+': '#FF9500', '=': '#FF9500',
}
btn_frame = tk.Frame(self.root, bg='#1e1e1e', padx=10, pady=5)
btn_frame.pack()
for row_buttons in buttons:
row_frame = tk.Frame(btn_frame, bg='#1e1e1e')
row_frame.pack(pady=3)
for btn_text in row_buttons:
if btn_text == '':
tk.Frame(row_frame, width=75, bg='#1e1e1e').pack(side='left', padx=3)
continue
color = btn_colors.get(btn_text, '#3a3a3a')
btn = tk.Button(row_frame, text=btn_text, width=5, height=2,
font=('Arial', 14, 'bold'), bg=color, fg='white',
relief='flat', cursor='hand2',
command=lambda b=btn_text: self._button_click(b))
btn.pack(side='left', padx=3)
def _button_click(self, text):
if text == 'C':
self.expression = ""
self.display_var.set("0")
elif text == '=':
try:
expr = self.expression.replace('×', '*').replace('÷', '/')
result = eval(expr)
if isinstance(result, float) and result.is_integer():
result = int(result)
self.display_var.set(result)
self.expression = str(result)
except ZeroDivisionError:
self.display_var.set("Error: ÷0")
self.expression = ""
except Exception:
self.display_var.set("Error")
self.expression = ""
elif text == '±':
if self.expression and self.expression[0] == '-':
self.expression = self.expression[1:]
elif self.expression:
self.expression = '-' + self.expression
self.display_var.set(self.expression or "0")
elif text == '%':
try:
val = float(self.expression) / 100
self.expression = str(val)
self.display_var.set(self.expression)
except ValueError:
pass
else:
if self.display_var.get() == "0" and text.isdigit():
self.expression = text
else:
self.expression += text
self.display_var.set(self.expression)
if __name__ == '__main__':
root = tk.Tk()
app = CalculatorGUI(root)
root.mainloop()
Running the Projects
# Version 1 — Basic
python calculator_v1.py
# Version 2 — Enhanced
python calculator_v2.py
# Version 3 — OOP with history
python calculator_v3.py
# Version 4 — GUI
python calculator_gui.py
Extension Challenges
- Add unit conversion (feet→meters, Celsius→Fahrenheit, etc.)
- Expression parser — allow typing
3 + 5 * 2 directly - Graph mode — plot
y = sin(x) using matplotlib - Currency calculator — fetch live exchange rates via API
- Matrix calculator — 2x2 and 3x3 matrix operations
Summary
In this project, you built:
- ✅ V1: Basic 4-function calculator with input validation
- ✅ V2: Enhanced calculator with scientific operations
- ✅ V3: Full OOP calculator with memory, history, and persistence
- ✅ V4: GUI calculator with tkinter
Key skills practiced:
- Functions and lambda expressions
- While loops and conditional logic
- Exception handling (try/except)
- Object-oriented programming
- File I/O (JSON persistence)
- tkinter GUI basics
*Next Project: Password Generator →*