Calculator GUI
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()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()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
- Python 3.6 or above.
- A text editor or IDE.
- Tkinter (ships with Python on Windows/macOS; Linux:
sudo apt install python3-tksudo apt install python3-tk). - Familiarity with functions, events, and basic Tkinter widgets.
Getting Started
Create the project
- Create folder
calculatorGUIcalculatorGUI. - Inside, create
calculatorgui.pycalculatorgui.py.
Write the code
Calculator GUI
Source# 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()
# 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
C:\Users\Your Name\calculatorGUI> python calculatorgui.py
# Window opens with:
# - Display at the top
# - Number pad (0-9)
# - Operator buttons (+, -, *, /)
# - = and Clear buttonsC:\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
1. Imports and state
from tkinter import *
gui = Tk()
gui.title("Calculator")
gui.configure(background="light green")
gui.geometry("270x180")
solution = ""
equation = StringVar()from tkinter import *
gui = Tk()
gui.title("Calculator")
gui.configure(background="light green")
gui.geometry("270x180")
solution = ""
equation = StringVar()solutionsolutionis the current expression as a string.StringVarStringVaris a Tkinter helper that lets a widget automatically refresh when the variable changes.
2. The display field
expression_field = Entry(gui, textvariable=equation)
expression_field.grid(columnspan=4, ipadx=70)expression_field = Entry(gui, textvariable=equation)
expression_field.grid(columnspan=4, ipadx=70)textvariable=equationtextvariable=equationbinds the entry to ourStringVarStringVar— any update toequationequationimmediately appears in the field.columnspan=4columnspan=4makes the entry span the full width of the four-column grid.
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("")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("")globalglobal is needed because the callbacks modify a module-level variable. (We will fix this with a class later.)
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)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)lambda v=label: press(v) — without the v=labelv=label default-argument capture, every button would call press(label)press(label) with the last label assigned in the loop. This is the classic Python closure trap.
5. Start the event loop
gui.mainloop()gui.mainloop()Why eval()eval() Is Dangerous
eval(input())eval(input())If the user types __import__('os').system('rm -rf /')__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
The astast 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)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 equalpressequalpress calls safe_eval(solution)safe_eval(solution) instead of eval(solution)eval(solution). import osimport os and any function call are immediately rejected.
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)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
Globals are fine for a 50-line script. Past that, wrap state on selfself:
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.
...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
| Problem | Cause | Fix |
|---|---|---|
| All buttons type the last label | Closure trap in the loop | lambda v=label: press(v)lambda v=label: press(v) |
5+35+3 produces errorerror | Used == immediately after typing — empty solution | Validate before evaluating |
| Division gives integer | Used //// or evaleval quirks | truedivtruediv from operatoroperator |
| 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()gui.focus_set() or bind on the entry widget |
Variations to Try
1. Backspace button
A ←← button that does solution = solution[:-1]solution = solution[:-1].
2. Scientific mode
Add buttons for sinsin, coscos, tantan, loglog, sqrtsqrt, pipi, ee. Use mathmath and route through the safe evaluator with a custom allow-list of function nodes.
3. Memory keys
M+M+, M-M-, MRMR, MCMC — store a single number in self.memoryself.memory.
4. Parentheses
Add (( and )) buttons; the AST evaluator handles them automatically via standard precedence.
5. History panel
A ListboxListbox on the right that records each evaluated expression. Double-click an entry to re-load it into the display.
6. Themes
A “Dark” toggle that switches bgbg and fgfg on every widget. Store the palette in a dict and apply it via a loop.
7. ANS variable
A button (or shortcut) that inserts the last result into the current expression.
8. Unit conversions
Add inches→cm, lbs→kg, etc. (See Temperature Converter for the pattern.)
9. Plotting
A PlotPlot button that pipes the current expression into matplotlib over a range of x values — a tiny graphing calculator.
10. CustomTkinter port
Replace tkintertkinter with customtkintercustomtkinter for modern rounded buttons and a true dark mode.
Architectural Notes
A real calculator separates four concerns:
- Tokenizer — turns
"3+4*2""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")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
- 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
- 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
evalevalis dangerous — and a real alternative. - State management — globals vs. instance attributes.
Next Steps
- Replace
eval()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 (
sinsin,coscos,sqrtsqrt). - 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
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
A working version of the same calculator — click the buttons to build an expression,
== to evaluate, CC to clear:
Conclusion
You built a real GUI calculator, learned the most-cited Python closure gotcha, and replaced a dangerous one-liner (evaleval) with a safe equivalent. The patterns — StringVarStringVar 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.
If this helped you, consider buying me a coffee ☕
Buy me a coffeeWas this page helpful?
Let us know how we did
