Calculator GUI
Abstract
Section titled “Abstract”A graphical calculator is the perfect “first real Tkinter app” — every button is a callback, every press updates one shared variable, and the result lights up immediately. In this project you will build a working calculator with a number pad, four operators, clear/equals buttons, and error handling. Then we will replace the dangerous eval() evaluator with a safe AST-based one, add keyboard input, expression history, and a scientific mode.
You will learn:
- The Tkinter grid layout in earnest.
- The lambda + default-argument trick for binding buttons in a loop.
- Why
eval()is dangerous and how to evaluate math expressions safely. - How to capture keyboard input alongside button clicks.
- How to keep a running history and let the user reuse prior results.
Prerequisites
Section titled “Prerequisites”- Python 3.6 or above.
- A text editor or IDE.
- Tkinter (ships with Python on Windows/macOS; Linux:
sudo apt install python3-tk). - Familiarity with functions, events, and basic Tkinter widgets.
Getting Started
Section titled “Getting Started”Create the project
Section titled “Create the project”- Create folder
calculatorGUI. - Inside, create
calculatorgui.py.
Write the code
Section titled “Write the code”Calculator GUI
pch.viewSource# Calculator with GUI using Tkinter
from tkinter import *
solution = ""
def press(num):
global solution
solution = solution + str(num)
equation.set(solution)
def equalpress():
try:
global solution
total = str(eval(solution))
equation.set(total)
solution = ""
except:
equation.set(" error ")
solution = ""
def clear():
global solution
solution = ""
equation.set("")
if __name__ == "__main__":
gui = Tk()
gui.configure(background="light green")
gui.title("Calculator")
gui.geometry("270x150")
equation = StringVar()
expression_field = Entry(gui, textvariable=equation)
expression_field.grid(columnspan=4, ipadx=70)
equation.set('enter your expression')
button1 = Button(gui, text=' 1 ', fg='black', bg='gray',
command=lambda: press(1), height=1, width=7)
button1.grid(row=2, column=0)
button2 = Button(gui, text=' 2 ', fg='black', bg='gray',
command=lambda: press(2), height=1, width=7)
button2.grid(row=2, column=1)
button3 = Button(gui, text=' 3 ', fg='black', bg='gray',
command=lambda: press(3), height=1, width=7)
button3.grid(row=2, column=2)
button4 = Button(gui, text=' 4 ', fg='black', bg='gray',
command=lambda: press(4), height=1, width=7)
button4.grid(row=3, column=0)
button5 = Button(gui, text=' 5 ', fg='black', bg='gray',
command=lambda: press(5), height=1, width=7)
button5.grid(row=3, column=1)
button6 = Button(gui, text=' 6 ', fg='black', bg='gray',
command=lambda: press(6), height=1, width=7)
button6.grid(row=3, column=2)
button7 = Button(gui, text=' 7 ', fg='black', bg='gray',
command=lambda: press(7), height=1, width=7)
button7.grid(row=4, column=0)
button8 = Button(gui, text=' 8 ', fg='black', bg='gray',
command=lambda: press(8), height=1, width=7)
button8.grid(row=4, column=1)
button9 = Button(gui, text=' 9 ', fg='black', bg='gray',
command=lambda: press(9), height=1, width=7)
button9.grid(row=4, column=2)
button0 = Button(gui, text=' 0 ', fg='black', bg='gray',
command=lambda: press(0), height=1, width=7)
button0.grid(row=5, column=0)
plus = Button(gui, text=' + ', fg='black', bg='gray',
command=lambda: press("+"), height=1, width=7)
plus.grid(row=2, column=3)
minus = Button(gui, text=' - ', fg='black', bg='gray',
command=lambda: press("-"), height=1, width=7)
minus.grid(row=3, column=3)
multiply = Button(gui, text=' * ', fg='black', bg='gray',
command=lambda: press("*"), height=1, width=7)
multiply.grid(row=4, column=3)
divide = Button(gui, text=' / ', fg='black', bg='gray',
command=lambda: press("/"), height=1, width=7)
divide.grid(row=5, column=3)
equal = Button(gui, text=' = ', fg='black', bg='gray',
command=equalpress, height=1, width=7)
equal.grid(row=5, column=2)
clear = Button(gui, text='Clear', fg='black', bg='gray',
command=clear, height=1, width=7)
clear.grid(row=5, column='1')
Decimal= Button(gui, text='.', fg='black', bg='gray',
command=lambda: press('.'), height=1, width=7)
Decimal.grid(row=6, column=0)
gui.mainloop() Run it
Section titled “Run it”C:\Users\Your Name\calculatorGUI> python calculatorgui.py
# Window opens with:
# - Display at the top
# - Number pad (0-9)
# - Operator buttons (+, -, *, /)
# - = and Clear buttonsStep-by-Step Explanation
Section titled “Step-by-Step Explanation”1. Imports and state
Section titled “1. Imports and state”from tkinter import *
gui = Tk()
gui.title("Calculator")
gui.configure(background="light green")
gui.geometry("270x180")
solution = ""
equation = StringVar()solutionis the current expression as a string.StringVaris a Tkinter helper that lets a widget automatically refresh when the variable changes.
2. The display field
Section titled “2. The display field”expression_field = Entry(gui, textvariable=equation)
expression_field.grid(columnspan=4, ipadx=70)textvariable=equationbinds the entry to ourStringVar— any update toequationimmediately appears in the field.columnspan=4makes the entry span the full width of the four-column grid.
3. Button callbacks
Section titled “3. Button callbacks”def press(value):
global solution
solution = solution + str(value)
equation.set(solution)
def equalpress():
global solution
try:
total = str(eval(solution))
equation.set(total)
solution = ""
except Exception:
equation.set(" error ")
solution = ""
def clear():
global solution
solution = ""
equation.set("")global is needed because the callbacks modify a module-level variable. (We will fix this with a class later.)
4. The button grid
Section titled “4. The button grid”buttons = [
("1", 2, 0), ("2", 2, 1), ("3", 2, 2), ("+", 2, 3),
("4", 3, 0), ("5", 3, 1), ("6", 3, 2), ("-", 3, 3),
("7", 4, 0), ("8", 4, 1), ("9", 4, 2), ("*", 4, 3),
("0", 5, 0), (".", 5, 1), ("=", 5, 2), ("/", 5, 3),
]
for label, row, col in buttons:
cmd = equalpress if label == "=" else (lambda v=label: press(v))
Button(gui, text=f" {label} ", fg="black", bg="gray",
command=cmd, height=1, width=7).grid(row=row, column=col)
Button(gui, text="Clear", command=clear, height=1, width=7).grid(row=6, columnspan=4)Notice lambda v=label: press(v) — without the v=label default-argument capture, every button would call press(label) with the last label assigned in the loop. This is the classic Python closure trap.
5. Start the event loop
Section titled “5. Start the event loop”gui.mainloop()Why eval() Is Dangerous
Section titled “Why eval() Is Dangerous”eval(input())If the user types __import__('os').system('rm -rf /'), Python obediently executes it. A pure-math input field is “safe” only as long as you trust whoever can type into it — in a multi-user app, in a web app, never.
Safe alternative: AST evaluator
Section titled “Safe alternative: AST evaluator”The ast module parses Python source without executing it. We can walk the tree manually and allow only math nodes:
import ast, operator as op
OPS = {
ast.Add: op.add, ast.Sub: op.sub,
ast.Mult: op.mul, ast.Div: op.truediv,
ast.Mod: op.mod, ast.Pow: op.pow,
ast.USub: op.neg,
}
def safe_eval(expr: str):
tree = ast.parse(expr, mode="eval")
def _eval(node):
if isinstance(node, ast.Expression):
return _eval(node.body)
if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
return node.value
if isinstance(node, ast.BinOp):
return OPS[type(node.op)](_eval(node.left), _eval(node.right))
if isinstance(node, ast.UnaryOp):
return OPS[type(node.op)](_eval(node.operand))
raise ValueError(f"Unsupported: {ast.dump(node)}")
return _eval(tree)Now equalpress calls safe_eval(solution) instead of eval(solution). import os and any function call are immediately rejected.
Keyboard Support
Section titled “Keyboard Support”Buttons are nice; typing is faster:
def on_key(event):
if event.char in "0123456789+-*/.":
press(event.char)
elif event.keysym in ("Return", "equal"):
equalpress()
elif event.keysym == "BackSpace":
global solution
solution = solution[:-1]
equation.set(solution)
elif event.keysym == "Escape":
clear()
gui.bind("<Key>", on_key)Refactor into a Class
Section titled “Refactor into a Class”Globals are fine for a 50-line script. Past that, wrap state on self:
class CalculatorApp:
def __init__(self, root):
self.solution = ""
self.equation = StringVar()
self.root = root
self._build()
def press(self, v):
self.solution += str(v); self.equation.set(self.solution)
def equalpress(self):
try:
self.equation.set(str(safe_eval(self.solution)))
except Exception:
self.equation.set("error")
self.solution = ""
def clear(self):
self.solution = ""; self.equation.set("")
def _build(self):
# …create widgets, bind callbacks to self.press, etc.
...Now you can run multiple calculator windows side-by-side without state leaks, and unit-test the logic without a window.
Common Mistakes
Section titled “Common Mistakes”| Problem | Cause | Fix |
|---|---|---|
| All buttons type the last label | Closure trap in the loop | lambda v=label: press(v) |
5+3 produces error | Used = immediately after typing — empty solution | Validate before evaluating |
| Division gives integer | Used // or eval quirks | truediv from operator |
| Decimal point appears twice | No check on . | Reject . if last operator-separated segment already has one |
| Keyboard does nothing | Focus is on a button | gui.focus_set() or bind on the entry widget |
Variations to Try
Section titled “Variations to Try”1. Backspace button
Section titled “1. Backspace button”A ← button that does solution = solution[:-1].
2. Scientific mode
Section titled “2. Scientific mode”Add buttons for sin, cos, tan, log, sqrt, pi, e. Use math and route through the safe evaluator with a custom allow-list of function nodes.
3. Memory keys
Section titled “3. Memory keys”M+, M-, MR, MC — store a single number in self.memory.
4. Parentheses
Section titled “4. Parentheses”Add ( and ) buttons; the AST evaluator handles them automatically via standard precedence.
5. History panel
Section titled “5. History panel”A Listbox on the right that records each evaluated expression. Double-click an entry to re-load it into the display.
6. Themes
Section titled “6. Themes”A “Dark” toggle that switches bg and fg on every widget. Store the palette in a dict and apply it via a loop.
7. ANS variable
Section titled “7. ANS variable”A button (or shortcut) that inserts the last result into the current expression.
8. Unit conversions
Section titled “8. Unit conversions”Add inches→cm, lbs→kg, etc. (See Temperature Converter for the pattern.)
9. Plotting
Section titled “9. Plotting”A Plot button that pipes the current expression into matplotlib over a range of x values — a tiny graphing calculator.
10. CustomTkinter port
Section titled “10. CustomTkinter port”Replace tkinter with customtkinter for modern rounded buttons and a true dark mode.
Architectural Notes
Section titled “Architectural Notes”A real calculator separates four concerns:
- Tokenizer — turns
"3+4*2"into a list of tokens. - Parser — turns tokens into a tree honoring precedence.
- Evaluator — walks the tree producing a number.
- UI — buttons, display, keys.
Step 1+2 is what ast.parse(expr, mode="eval") does for you for free. The AST evaluator above is the smallest possible step 3. The UI is everything we built with Tkinter.
Real-World Applications
Section titled “Real-World Applications”- Embedded developer tools (a calculator panel inside a larger app).
- Educational software for arithmetic / algebra.
- Field calculators in surveying, engineering, finance apps.
- Touch-screen kiosks for retail (use bigger buttons, no keyboard).
Educational Value
Section titled “Educational Value”- Tkinter grid layout — rows, columns, spans.
- Event-driven UI — every button is a callback.
- Closures and the loop-variable trap — a Python-wide gotcha you should know.
- Why
evalis dangerous — and a real alternative. - State management — globals vs. instance attributes.
Next Steps
Section titled “Next Steps”- Replace
eval()with the safe AST evaluator above. (Do this even if no untrusted user can type.) - Add keyboard bindings.
- Refactor into a class.
- Add history and an ANS button.
- Add scientific functions (
sin,cos,sqrt). - Port the UI to CustomTkinter or PyQt when you outgrow Tkinter’s defaults.
- See Simple Calculator for the CLI version with the same math under the hood.
Visualize it
Section titled “Visualize it”Here’s the event loop that turns button clicks into a displayed result.
flowchart LR A["User clicks a button"] --> B["Event handler updates expression"] B --> C["On \"=\" evaluate expression"] C --> D["Display the result"]
Try it here
Section titled “Try it here”A working version of the same calculator — click the buttons to build an expression,
= to evaluate, C to clear:
Conclusion
Section titled “Conclusion”You built a real GUI calculator, learned the most-cited Python closure gotcha, and replaced a dangerous one-liner (eval) with a safe equivalent. The patterns — StringVar for live binding, grid layout, lambda-with-default for loop callbacks — show up in every Tkinter app from here forward. Full source on GitHub. Find more GUI projects on Python Central Hub.
pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading