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 -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-tkCore 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 documentationStep 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.pyA 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 |
Related Projects
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: