Skip to content

Calculator GUI

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.
  • 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.
  1. Create folder calculatorGUI.
  2. Inside, create calculatorgui.py.
Calculator GUI pch.viewSource
Calculator GUI
# 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()
command
C:\Users\Your Name\calculatorGUI> python calculatorgui.py
# Window opens with:
#   - Display at the top
#   - Number pad (0-9)
#   - Operator buttons (+, -, *, /)
#   - = and Clear buttons
calculatorgui.py
from tkinter import *
gui = Tk()
gui.title("Calculator")
gui.configure(background="light green")
gui.geometry("270x180")
 
solution = ""
equation = StringVar()
  • solution is the current expression as a string.
  • StringVar is a Tkinter helper that lets a widget automatically refresh when the variable changes.
calculatorgui.py
expression_field = Entry(gui, textvariable=equation)
expression_field.grid(columnspan=4, ipadx=70)
  • textvariable=equation binds the entry to our StringVar — any update to equation immediately appears in the field.
  • columnspan=4 makes the entry span the full width of the four-column grid.
calculatorgui.py
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.)

calculatorgui.py
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.

calculatorgui.py
gui.mainloop()
dangerous.py
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.

The ast module parses Python source without executing it. We can walk the tree manually and allow only math nodes:

safe_eval.py
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.

Buttons are nice; typing is faster:

keys.py
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)

Globals are fine for a 50-line script. Past that, wrap state on self:

oop.py
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.

ProblemCauseFix
All buttons type the last labelClosure trap in the looplambda v=label: press(v)
5+3 produces errorUsed = immediately after typing — empty solutionValidate before evaluating
Division gives integerUsed // or eval quirkstruediv from operator
Decimal point appears twiceNo check on .Reject . if last operator-separated segment already has one
Keyboard does nothingFocus is on a buttongui.focus_set() or bind on the entry widget

A button that does solution = solution[:-1].

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.

M+, M-, MR, MC — store a single number in self.memory.

Add ( and ) buttons; the AST evaluator handles them automatically via standard precedence.

A Listbox on the right that records each evaluated expression. Double-click an entry to re-load it into the display.

A “Dark” toggle that switches bg and fg on every widget. Store the palette in a dict and apply it via a loop.

A button (or shortcut) that inserts the last result into the current expression.

Add inches→cm, lbs→kg, etc. (See Temperature Converter for the pattern.)

A Plot button that pipes the current expression into matplotlib over a range of x values — a tiny graphing calculator.

Replace tkinter with customtkinter for modern rounded buttons and a true dark mode.

A real calculator separates four concerns:

  1. Tokenizer — turns "3+4*2" into a list of tokens.
  2. Parser — turns tokens into a tree honoring precedence.
  3. Evaluator — walks the tree producing a number.
  4. 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.

  • 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).
  • 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 eval is dangerous — and a real alternative.
  • State management — globals vs. instance attributes.
  • 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.

Here’s the event loop that turns button clicks into a displayed result.

diagram Calculator GUI event flow mermaid
How a button click becomes a displayed result in the Tkinter loop.

A working version of the same calculator — click the buttons to build an expression, = to evaluate, C to clear:

sketch A working calculator p5.js
Click the buttons to build an expression; press = to evaluate, C to clear.

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.coffeeCta

pch.feedbackHeading

pch.feedbackSubheading