DevelopmentJanuary 25, 20258 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

FrameworkBuilt-inLearning CurveLook & FeelBest For
Tkinter✅ YesEasyNative OSBeginners, small tools
PyQt6❌ NoModeratePolishedProfessional desktop apps
wxPython❌ NoModerateNative OSCross-platform apps
Kivy❌ NoSteepCustomMobile + touch apps
Dear PyGui❌ NoModerateGPU-renderedData dashboards
CustomTkinter❌ NoEasyModern/darkModern-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:
  • code
    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:

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

    WidgetClassPurpose in This Project
    Main windowTk()The root application window
    Display frameFrameGroups the display widgets
    Expression labelLabelShows the current input expression
    Result labelLabelShows the computed result
    ButtonButtonEach calculator key (0–9, +, -, etc.)
    Grid layoutgrid()Arranges buttons in rows and columns
    History frameFrame + TextScrollable calculation history log
    ScrollbarScrollbarScrolls the history log

    Project Structure

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

    Step 1: Create the Main Window

    Create calculator.py and set up the root window:

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

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

    code
    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

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

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

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

    code
    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

    code
    python calculator.py

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


    Key Tkinter Concepts Explained

    ConceptHow It Is Used Here
    Tk()Creates the root application window
    FrameGroups related widgets (display, history)
    LabelShows the expression and result text
    ButtonEach key on the calculator
    StringVarLinks a Python variable to a Label — update the variable and the UI updates automatically
    grid()Positions widgets in a row/column table
    columnspanMakes the "0" button span two columns
    bind()Attaches keyboard events to handler functions
    TextMulti-line, scrollable history log
    ScrollbarAdds 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 0Col 1Col 2Col 3
    AC+/-%/
    789*
    456-
    123+
    0 (span 2).=

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


    Extending the Calculator

    Here are ideas for taking this project further:

    FeatureImplementation Hint
    Scientific modeAdd sin, cos, sqrt buttons using Python's math module
    Dark / light theme toggleStore colors in a dict and apply on button click
    Copy result to clipboardUse root.clipboard_clear() and root.clipboard_append(result)
    Export history to fileWrite self.history list to a .txt file using Python's open()
    Custom button soundsUse 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:

  • Python Tkinter Documentation
  • TkDocs — Modern Tkinter Guide
  • CustomTkinter — Drop-in replacement for Tkinter with a modern look
  • Tk Command Reference