Python Central HubSign in

Python Projects / Beginner

Calculator GUI

Build a graphical calculator in Python with Tkinter. Learn grid layout, button callbacks, expression evaluation, why eval() is dangerous, AST-based safe evaluation, keyboard support, and themes.

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

  • 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

Create the project

  1. Create folder calculatorGUI.
  2. Inside, create calculatorgui.py.

Write the code

Calculator GUISource
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()

Run it

cmd
C:\Users\Your Name\calculatorGUI> python calculatorgui.py
# Window opens with:
#   - Display at the top
#   - Number pad (0-9)
#   - Operator buttons (+, -, *, /)
#   - = and Clear buttons

Step-by-Step Explanation

1. Imports and state

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

2. The display field

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

3. Button callbacks

Python
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

Python
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

Python
gui.mainloop()

Why eval() Is Dangerous

Python
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

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

Python
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

Buttons are nice; typing is faster:

Python
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 self:

Python
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

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

Variations to Try

1. Backspace button

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

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

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

4. Parentheses

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

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

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

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 Plot button that pipes the current expression into matplotlib over a range of x values — a tiny graphing calculator.

10. CustomTkinter port

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

Architectural Notes

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.

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

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

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.

Try it here

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.

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.

Was this page helpful?

Let us know how we did