Tech With Galvan
Back to Articles
Development1/25/2025 • 8 min read

GUI Calculator using Python and Tkinter

Build a fully functional GUI calculator with Python and Tkinter — complete with keyboard support, error handling, history log, and a clean modern layout.

Galvan
Galvan

Founder & Creator

Introduction

Building a Graphical User Interface (GUI) application is a milestone moment for any Python developer. It is the point where your code stops being invisible terminal output and becomes something anyone can pick up and use without knowing a single line of Python.

Tkinter is Python’s built-in GUI library — it ships with every standard Python installation, which means you need zero extra dependencies to build desktop apps with it. In this tutorial you will build a fully functional calculator with a clean layout, keyboard support, proper error handling, and a calculation history log.

If you enjoy building Python projects, you might also like the Age Calculator App (a web-based date calculator built with Streamlit) or the Password Generator (another beginner-friendly Python project).


What is Tkinter?

Tkinter (short for Tk interface) is the standard Python binding to the Tk GUI toolkit. It has been bundled with CPython since version 1.5.2 and remains the most widely used GUI framework for Python today.

Tkinter vs Other Python GUI Frameworks

Framework Built-in Learning Curve Look & Feel Best For
Tkinter ✅ Yes Easy Native OS Beginners, small tools
PyQt6 ❌ No Moderate Polished Professional desktop apps
wxPython ❌ No Moderate Native OS Cross-platform apps
Kivy ❌ No Steep Custom Mobile + touch apps
Dear PyGui ❌ No Moderate GPU-rendered Data dashboards
CustomTkinter ❌ No Easy Modern/dark Modern-looking Tkinter apps

For a simple calculator, Tkinter is the perfect choice: zero setup, zero cost, and it works identically on Windows, macOS, and Linux.


Prerequisites

Tkinter comes pre-installed with Python. You just need:

  • Python 3.8+ — Download from python.org.
  • Verify Tkinter is available — run this in your terminal:
python -c "import tkinter; print(tkinter.TkVersion)"

If you see a version number (e.g. 8.6), you are ready to go. If you get an error on Linux, install it with:

sudo apt-get install python3-tk

Core Tkinter Widgets Used

Before diving into the code, here is a quick reference of every Tkinter widget used in this project:

Widget Class Purpose in This Project
Main window Tk() The root application window
Display frame Frame Groups the display widgets
Expression label Label Shows the current input expression
Result label Label Shows the computed result
Button Button Each calculator key (0–9, +, -, etc.)
Grid layout grid() Arranges buttons in rows and columns
History frame Frame + Text Scrollable calculation history log
Scrollbar Scrollbar Scrolls the history log

Project Structure

calculator/
├── calculator.py     ← Main application
└── README.md         ← Optional documentation

Step 1: Create the Main Window

Create calculator.py and set up the root window:

import tkinter as tk
from tkinter import font as tkfont

class Calculator:
    def __init__(self, root):
        self.root = root
        self.root.title("Python Calculator")
        self.root.resizable(False, False)
        self.root.configure(bg="#1e1e2e")

        self.expression = ""
        self.history = []

        self._build_display()
        self._build_buttons()
        self._build_history()
        self._bind_keyboard()

if __name__ == "__main__":
    root = tk.Tk()
    app = Calculator(root)
    root.mainloop()

Using a class-based structure keeps the code organised as the app grows. self.expression stores the current input string, and self.history tracks all previous calculations.


Step 2: Build the Display

The display has two labels: one showing the current expression being typed and a larger one showing the result.

def _build_display(self):
    display_frame = tk.Frame(self.root, bg="#1e1e2e", padx=10, pady=10)
    display_frame.grid(row=0, column=0, columnspan=4, sticky="nsew")

    self.expr_var = tk.StringVar(value="")
    self.result_var = tk.StringVar(value="0")

    expr_label = tk.Label(
        display_frame,
        textvariable=self.expr_var,
        font=("Consolas", 14),
        bg="#1e1e2e",
        fg="#888aaa",
        anchor="e",
        width=22,
    )
    expr_label.pack(fill="x")

    result_label = tk.Label(
        display_frame,
        textvariable=self.result_var,
        font=("Consolas", 32, "bold"),
        bg="#1e1e2e",
        fg="#cdd6f4",
        anchor="e",
        width=22,
    )
    result_label.pack(fill="x")

Using tk.StringVar() means you can update the label text at any time just by calling self.result_var.set(new_value) — Tkinter automatically refreshes the UI.


Step 3: Define the Button Layout

The calculator has 5 rows of buttons. Each button definition is a tuple of (label, column, row, colspan, color):

def _build_buttons(self):
    BUTTONS = [
        # Row 1 — utility
        ("AC",  0, 1, 1, "#f38ba8"),
        ("+/-", 1, 1, 1, "#6c7086"),
        ("%",   2, 1, 1, "#6c7086"),
        ("/",   3, 1, 1, "#fab387"),
        # Row 2
        ("7",   0, 2, 1, "#313244"),
        ("8",   1, 2, 1, "#313244"),
        ("9",   2, 2, 1, "#313244"),
        ("*",   3, 2, 1, "#fab387"),
        # Row 3
        ("4",   0, 3, 1, "#313244"),
        ("5",   1, 3, 1, "#313244"),
        ("6",   2, 3, 1, "#313244"),
        ("-",   3, 3, 1, "#fab387"),
        # Row 4
        ("1",   0, 4, 1, "#313244"),
        ("2",   1, 4, 1, "#313244"),
        ("3",   2, 4, 1, "#313244"),
        ("+",   3, 4, 1, "#fab387"),
        # Row 5
        ("0",   0, 5, 2, "#313244"),  # colspan=2
        (".",   2, 5, 1, "#313244"),
        ("=",   3, 5, 1, "#a6e3a1"),
    ]

    for (text, col, row, colspan, color) in BUTTONS:
        btn = tk.Button(
            self.root,
            text=text,
            font=("Consolas", 18, "bold"),
            bg=color,
            fg="#cdd6f4",
            activebackground="#45475a",
            activeforeground="#cdd6f4",
            relief="flat",
            width=4,
            height=2,
            command=lambda t=text: self._on_button(t),
        )
        btn.grid(row=row, column=col, columnspan=colspan,
                 sticky="nsew", padx=2, pady=2)

The lambda t=text: pattern is important here. Without t=text, all lambdas would capture the same loop variable text and every button would trigger the same action.


Step 4: Handle Button Logic

def _on_button(self, key):
    if key == "AC":
        self.expression = ""
        self.expr_var.set("")
        self.result_var.set("0")

    elif key == "+/-":
        if self.expression:
            if self.expression.startswith("-"):
                self.expression = self.expression[1:]
            else:
                self.expression = "-" + self.expression
            self.expr_var.set(self.expression)

    elif key == "%":
        try:
            value = float(self.expression)
            self.expression = str(value / 100)
            self.expr_var.set(self.expression)
        except:
            pass

    elif key == "=":
        self._evaluate()

    else:
        self.expression += key
        self.expr_var.set(self.expression)

def _evaluate(self):
    try:
        result = eval(self.expression)
        # Format nicely: remove unnecessary .0
        if isinstance(result, float) and result.is_integer():
            result = int(result)
        result_str = str(result)
        self.history.append(f"{self.expression} = {result_str}")
        self._update_history()
        self.result_var.set(result_str)
        self.expression = result_str
        self.expr_var.set("")
    except ZeroDivisionError:
        self.result_var.set("Cannot divide by 0")
        self.expression = ""
    except Exception:
        self.result_var.set("Error")
        self.expression = ""

Note on eval(): Using Python’s built-in eval() is convenient for a personal tool, but you should never use eval() on untrusted user input in a production or web application — it can execute arbitrary Python code. For a local desktop calculator it is perfectly fine.


Step 5: Add Keyboard Support

A calculator is much more pleasant to use when you can type from the keyboard:

def _bind_keyboard(self):
    bindings = {
        "<Return>": "=",
        "<KP_Enter>": "=",
        "<BackSpace>": "BACK",
        "<Escape>": "AC",
    }
    for key, action in bindings.items():
        self.root.bind(key, lambda e, a=action: self._on_button(a))

    # Number keys and operators
    for char in "0123456789+-*/.%":
        self.root.bind(char, lambda e, c=char: self._on_button(c))

    # Handle Backspace in _on_button
def _on_button(self, key):
    if key == "BACK":
        self.expression = self.expression[:-1]
        self.expr_var.set(self.expression)
        return
    # ... (rest of the logic from Step 4)

Step 6: Add a Calculation History Panel

A history panel shows the last 10 calculations so users can reference previous results:

def _build_history(self):
    history_frame = tk.Frame(self.root, bg="#181825", padx=5, pady=5)
    history_frame.grid(row=0, column=4, rowspan=6, sticky="nsew", padx=(5, 0))

    tk.Label(
        history_frame,
        text="History",
        font=("Consolas", 11, "bold"),
        bg="#181825",
        fg="#888aaa",
    ).pack(anchor="w")

    self.history_text = tk.Text(
        history_frame,
        width=22,
        font=("Consolas", 10),
        bg="#181825",
        fg="#cdd6f4",
        state="disabled",
        relief="flat",
    )
    scrollbar = tk.Scrollbar(history_frame, command=self.history_text.yview)
    self.history_text.configure(yscrollcommand=scrollbar.set)
    scrollbar.pack(side="right", fill="y")
    self.history_text.pack(fill="both", expand=True)

def _update_history(self):
    self.history_text.configure(state="normal")
    self.history_text.delete("1.0", "end")
    for entry in self.history[-10:][::-1]:
        self.history_text.insert("end", entry + "\n")
    self.history_text.configure(state="disabled")

Complete calculator.py

Here is the full, production-ready file:

import tkinter as tk

class Calculator:
    def __init__(self, root):
        self.root = root
        self.root.title("Python Calculator")
        self.root.resizable(False, False)
        self.root.configure(bg="#1e1e2e")
        self.expression = ""
        self.history = []
        self._build_display()
        self._build_buttons()
        self._build_history()
        self._bind_keyboard()

    def _build_display(self):
        frame = tk.Frame(self.root, bg="#1e1e2e", padx=10, pady=10)
        frame.grid(row=0, column=0, columnspan=4, sticky="nsew")
        self.expr_var = tk.StringVar(value="")
        self.result_var = tk.StringVar(value="0")
        tk.Label(frame, textvariable=self.expr_var, font=("Consolas", 14),
                 bg="#1e1e2e", fg="#888aaa", anchor="e", width=22).pack(fill="x")
        tk.Label(frame, textvariable=self.result_var, font=("Consolas", 32, "bold"),
                 bg="#1e1e2e", fg="#cdd6f4", anchor="e", width=22).pack(fill="x")

    def _build_buttons(self):
        BUTTONS = [
            ("AC",0,1,1,"#f38ba8"),("+/-",1,1,1,"#6c7086"),("%",2,1,1,"#6c7086"),("/",3,1,1,"#fab387"),
            ("7",0,2,1,"#313244"),("8",1,2,1,"#313244"),("9",2,2,1,"#313244"),("*",3,2,1,"#fab387"),
            ("4",0,3,1,"#313244"),("5",1,3,1,"#313244"),("6",2,3,1,"#313244"),("-",3,3,1,"#fab387"),
            ("1",0,4,1,"#313244"),("2",1,4,1,"#313244"),("3",2,4,1,"#313244"),("+",3,4,1,"#fab387"),
            ("0",0,5,2,"#313244"),("." ,2,5,1,"#313244"),("=",3,5,1,"#a6e3a1"),
        ]
        for (text, col, row, span, color) in BUTTONS:
            tk.Button(self.root, text=text, font=("Consolas", 18, "bold"),
                      bg=color, fg="#cdd6f4", activebackground="#45475a",
                      relief="flat", width=4, height=2,
                      command=lambda t=text: self._on_button(t)
                      ).grid(row=row, column=col, columnspan=span,
                             sticky="nsew", padx=2, pady=2)

    def _on_button(self, key):
        if key == "AC":
            self.expression = ""; self.expr_var.set(""); self.result_var.set("0")
        elif key == "BACK":
            self.expression = self.expression[:-1]; self.expr_var.set(self.expression)
        elif key == "+/-":
            if self.expression:
                self.expression = self.expression[1:] if self.expression.startswith("-") else "-"+self.expression
                self.expr_var.set(self.expression)
        elif key == "%":
            try: self.expression = str(float(self.expression)/100); self.expr_var.set(self.expression)
            except: pass
        elif key == "=":
            self._evaluate()
        else:
            self.expression += key; self.expr_var.set(self.expression)

    def _evaluate(self):
        try:
            result = eval(self.expression)
            if isinstance(result, float) and result.is_integer(): result = int(result)
            rs = str(result)
            self.history.append(f"{self.expression} = {rs}")
            self._update_history()
            self.result_var.set(rs)
            self.expression = rs; self.expr_var.set("")
        except ZeroDivisionError:
            self.result_var.set("Cannot divide by 0"); self.expression = ""
        except:
            self.result_var.set("Error"); self.expression = ""

    def _build_history(self):
        frame = tk.Frame(self.root, bg="#181825", padx=5, pady=5)
        frame.grid(row=0, column=4, rowspan=6, sticky="nsew", padx=(5,0))
        tk.Label(frame, text="History", font=("Consolas",11,"bold"),
                 bg="#181825", fg="#888aaa").pack(anchor="w")
        self.history_text = tk.Text(frame, width=22, font=("Consolas",10),
                                    bg="#181825", fg="#cdd6f4", state="disabled", relief="flat")
        sb = tk.Scrollbar(frame, command=self.history_text.yview)
        self.history_text.configure(yscrollcommand=sb.set)
        sb.pack(side="right", fill="y")
        self.history_text.pack(fill="both", expand=True)

    def _update_history(self):
        self.history_text.configure(state="normal")
        self.history_text.delete("1.0", "end")
        for entry in self.history[-10:][::-1]:
            self.history_text.insert("end", entry+"\n")
        self.history_text.configure(state="disabled")

    def _bind_keyboard(self):
        for char in "0123456789+-*/.%":
            self.root.bind(char, lambda e, c=char: self._on_button(c))
        self.root.bind("<Return>", lambda e: self._on_button("="))
        self.root.bind("<KP_Enter>", lambda e: self._on_button("="))
        self.root.bind("<BackSpace>", lambda e: self._on_button("BACK"))
        self.root.bind("<Escape>", lambda e: self._on_button("AC"))

if __name__ == "__main__":
    root = tk.Tk()
    Calculator(root)
    root.mainloop()

Run the Calculator

python calculator.py

A window will open immediately — no browser, no server, no extra setup needed.


Key Tkinter Concepts Explained

Concept How It Is Used Here
Tk() Creates the root application window
Frame Groups related widgets (display, history)
Label Shows the expression and result text
Button Each key on the calculator
StringVar Links a Python variable to a Label — update the variable and the UI updates automatically
grid() Positions widgets in a row/column table
columnspan Makes the “0” button span two columns
bind() Attaches keyboard events to handler functions
Text Multi-line, scrollable history log
Scrollbar Adds vertical scrolling to the history log
mainloop() Starts the Tkinter event loop — keeps the window open and responsive

Layout Reference

The button grid follows a standard calculator layout:

Col 0 Col 1 Col 2 Col 3
AC +/- % /
7 8 9 *
4 5 6 -
1 2 3 +
0 (span 2) . =

The history panel occupies column 4, spanning all 6 rows.


Extending the Calculator

Here are ideas for taking this project further:

Feature Implementation Hint
Scientific mode Add sin, cos, sqrt buttons using Python’s math module
Dark / light theme toggle Store colors in a dict and apply on button click
Copy result to clipboard Use root.clipboard_clear() and root.clipboard_append(result)
Export history to file Write self.history list to a .txt file using Python’s open()
Custom button sounds Use winsound (Windows) or pygame for click audio
Memory (M+, M-, MR) Add self.memory variable and corresponding buttons

  • Password Generator — Another beginner-friendly Python project; this time generating secure random passwords in a web app.
  • Age Calculator App — A date-based calculator built with Streamlit instead of Tkinter — great for comparing the two approaches.
  • Interactive Quiz App — Uses Streamlit’s session_state for state management, just as this Tkinter app uses instance variables.

Conclusion

You have built a complete, polished GUI calculator in Python using only the standard library. The app includes a two-part display, a full button grid, keyboard bindings, robust error handling, and a scrollable history panel — all without installing a single external package.

Tkinter is an excellent tool for personal utilities, data-entry forms, visualisation dashboards, and any application where you want a native desktop window without the complexity of a larger framework.

Resources: