Skip to content

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

  1. Create folder calculatorGUIcalculatorGUI.
  2. Inside, create calculatorgui.pycalculatorgui.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()
    
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

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

Step-by-Step Explanation

1. Imports and state

calculatorgui.py
from tkinter import *
gui = Tk()
gui.title("Calculator")
gui.configure(background="light green")
gui.geometry("270x180")
 
solution = ""
equation = StringVar()
calculatorgui.py
from tkinter import *
gui = Tk()
gui.title("Calculator")
gui.configure(background="light green")
gui.geometry("270x180")
 
solution = ""
equation = StringVar()
  • solutionsolution is the current expression as a string.
  • StringVarStringVar is a Tkinter helper that lets a widget automatically refresh when the variable changes.

2. The display field

calculatorgui.py
expression_field = Entry(gui, textvariable=equation)
expression_field.grid(columnspan=4, ipadx=70)
calculatorgui.py
expression_field = Entry(gui, textvariable=equation)
expression_field.grid(columnspan=4, ipadx=70)
  • textvariable=equationtextvariable=equation binds the entry to our StringVarStringVar — any update to equationequation immediately appears in the field.
  • columnspan=4columnspan=4 makes the entry span the full width of the four-column grid.

3. Button callbacks

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("")
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("")

globalglobal is needed because the callbacks modify a module-level variable. (We will fix this with a class later.)

4. The button grid

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)
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)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

calculatorgui.py
gui.mainloop()
calculatorgui.py
gui.mainloop()

Why eval()eval() Is Dangerous

dangerous.py
eval(input())
dangerous.py
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:

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)
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 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:

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)
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)

Refactor into a Class

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

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

Common Mistakes

ProblemCauseFix
All buttons type the last labelClosure trap in the looplambda v=label: press(v)lambda v=label: press(v)
5+35+3 produces errorerrorUsed == immediately after typing — empty solutionValidate before evaluating
Division gives integerUsed //// or evaleval quirkstruedivtruediv from operatoroperator
Decimal point appears twiceNo check on ..Reject .. if last operator-separated segment already has one
Keyboard does nothingFocus is on a buttongui.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:

  1. Tokenizer — turns "3+4*2""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")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 evaleval is 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.

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, CC 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 (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 coffee

Was this page helpful?

Let us know how we did