Every Python script you have written so far probably lives in a terminal, printing lines of text and waiting for typed input. Python Tkinter changes that: it puts a real window on the screen, one with buttons and text fields you click with your mouse. There is nothing to install, since it ships with Python itself. In this tutorial you build one from scratch, right up to a working calculator.
“The best interface is no interface. The second best is one you built yourself.”
Golden Krishna
Last Updated: July 2026 | Tested on: Python 3.14.6 | Difficulty: Intermediate | Reading Time: 17 minutes
Tkinter is Python’s built-in GUI (Graphical User Interface) toolkit. It ships with every Python install, so there is nothing to pip install. You create a window, add widgets (buttons, labels, text fields), arrange them with layout managers, and bind functions to events like clicks and key presses. The payoff is a real desktop application with a graphical interface that runs on Windows, macOS, and Linux.
Think of building a GUI like setting a dinner table. The window is the table. Widgets (the plates, glasses, and cutlery) are the things you put on it. The layout manager is the rule you follow for placing them: line them up in a row, or set them in a neat grid. And the event loop is the waiter, always standing by, watching for someone to pick something up so it can react. Once that picture clicks, the rest is just naming the widgets.
Let us be honest about looks. Tkinter is not the prettiest framework, and by default it has a slightly retro, 1990s feel. But it is the fastest way to put a window on a Python script, it has zero dependencies, and it teaches the event-driven fundamentals that carry over to every other GUI framework (Qt, Kivy, even web front ends). One of our workshop students, Prathamesh, shipped his first Tkinter app, a tiny unit converter, in under an hour. Watching a window pop up and react to your clicks is genuinely fun, even after years of terminal-only work.
Tip: click the diagram to open it full screen, then use the zoom and pan controls for a closer look.
The diagram shows Tkinter’s widget hierarchy. A root window holds frames, and each frame holds labels, entries, buttons, and other widgets that a geometry manager (pack, grid, or place) lines up for you. Every widget has a parent, so the whole app forms a tree: events bubble up from a clicked button toward the root, and layout flows down from the root toward each widget. This parent and child structure is the one idea you must hold in your head, because Tkinter’s layout system, and grid() with its row and column positions in particular, only makes sense once you know which widget belongs to which parent.
Table of Contents
Prerequisites
You should be comfortable with the functions tutorial and the classes and objects tutorial, since the calculator is one class with several methods. A rough idea of event-driven programming helps too: an event (a click, a key press) triggers a callback function. As for setup, there is nothing to install. Python Tkinter ships with the standard CPython installer on Windows and macOS. On Linux you may need the system package python3-tk (for example sudo apt install python3-tk). To check, run python -m tkinter and a small demo window should pop up.
Your First Window
Every Python Tkinter app is the same four lines wearing different clothes: make a root window, add some widgets, lay them out, then call mainloop(). Here is the smallest version that actually does something. It shows a label and a button, and each click updates the label text.
📄 hello_gui.py: a window with a label and a button
import tkinter as tk
# Create the root window
root = tk.Tk()
root.title("My First GUI")
root.geometry("400x200")
# Add a label
label = tk.Label(root, text="Hello, Tkinter!", font=("Arial", 24))
label.pack(pady=20)
# Add a button that changes the label
click_count = 0
def on_click():
global click_count
click_count += 1
label.config(text=f"Clicked {click_count} times!")
button = tk.Button(root, text="Click Me", command=on_click, font=("Arial", 14))
button.pack(pady=10)
# Start the event loop (keeps the window open)
root.mainloop()
A GUI does not print to the terminal, so there is no text output to paste. Instead, here is what the label shows as you click. (We verified this by triggering the button three times in code: the label starts as the greeting and counts up.)
▶ What the label reads
On startup: Hello, Tkinter! After 1 click: Clicked 1 times! After 3 clicks: Clicked 3 times!
What happened here: tk.Tk() creates the main window. .pack() drops each widget into the window, stacking them top to bottom. command=on_click ties the button to a callback, so the function runs every time the button is pressed. root.mainloop() starts the event loop, which is the line that keeps the window alive and listening. Forget mainloop() and the window would flash open and close in the same instant. One detail people miss: click_count lives outside the function, so we reach for global click_count to update it. Without that line, Python would treat click_count as a fresh local name and the counter would never climb past 1.
Core Widgets
Widgets make instant sense if you picture a paper form at a bank: the printed instructions are labels, the blank boxes you write in are entries, the tick boxes are checkbuttons, and the counter where you hand it over is the submit button. On screen it is the same cast. A label shows text. An entry takes typed text. A combobox is a dropdown. A checkbutton is a yes or no toggle. Those four cover most simple forms. The example below gathers a name, a city, and a newsletter choice, then pops up a message box when you click Submit.
📄 widgets_demo.py: common Tkinter widgets on one form
import tkinter as tk
from tkinter import ttk, messagebox
root = tk.Tk()
root.title("Widget Gallery")
root.geometry("500x400")
# Entry (text field)
tk.Label(root, text="Name:").pack(anchor="w", padx=10, pady=(10, 0))
name_entry = tk.Entry(root, width=40)
name_entry.pack(padx=10, pady=5)
# Dropdown (Combobox)
tk.Label(root, text="City:").pack(anchor="w", padx=10)
city_combo = ttk.Combobox(root, values=["Mumbai", "Pune", "Delhi", "Bangalore"])
city_combo.pack(padx=10, pady=5)
city_combo.set("Mumbai")
# Checkbox
newsletter_var = tk.BooleanVar()
tk.Checkbutton(root, text="Subscribe to newsletter", variable=newsletter_var).pack(padx=10, pady=5)
# Button
def submit():
name = name_entry.get()
city = city_combo.get()
sub = "Yes" if newsletter_var.get() else "No"
messagebox.showinfo("Submitted", f"Name: {name}\nCity: {city}\nNewsletter: {sub}")
tk.Button(root, text="Submit", command=submit).pack(pady=20)
root.mainloop()
Say a user named Rahul Mahadik fills the form: he types his name, picks Pune from the dropdown, ticks the newsletter box, and clicks Submit. The message box reads back exactly what the form collected:
▶ Message box contents
Name: Rahul Mahadik City: Pune Newsletter: Yes
What happened here: Each widget hands its current value back through a getter. name_entry.get() reads whatever is typed in the entry, city_combo.get() reads the selected dropdown value, and newsletter_var.get() returns a real True or False from the BooleanVar. That BooleanVar is the Tkinter habit worth learning: instead of poking the checkbox widget directly, you tie it to a variable object and read the variable. The same trick powers the calculator display later, where a StringVar holds the number on screen.
Layout Managers: pack, grid, place
A layout manager decides where each widget sits. Think of a bookshelf, a chessboard, and a corkboard: pack stacks widgets one after another like books on a shelf, grid gives each one a row and column like squares on a chessboard, and place pins a widget to exact coordinates like a note on a corkboard. Tkinter gives you all three, and you pick one per container. A login form is two labels next to two entries with a button underneath, which is a grid if there ever was one.
📄 grid_layout.py: a login form built with grid
import tkinter as tk
root = tk.Tk()
root.title("Login Form")
# Grid layout: row, column
tk.Label(root, text="Username:").grid(row=0, column=0, padx=10, pady=10, sticky="e")
tk.Entry(root, width=25).grid(row=0, column=1, padx=10, pady=10)
tk.Label(root, text="Password:").grid(row=1, column=0, padx=10, pady=10, sticky="e")
tk.Entry(root, width=25, show="*").grid(row=1, column=1, padx=10, pady=10)
tk.Button(root, text="Login", width=15).grid(row=2, column=0, columnspan=2, pady=20)
root.mainloop()
pack() stacks widgets vertically or horizontally. It is simple, but limited once a layout gets even slightly complex. grid() arranges widgets in rows and columns, which makes it the right call for forms and any structured layout. place() uses absolute pixel coordinates, so skip it unless you genuinely need a widget pinned to an exact spot. One rule keeps you out of trouble: never mix pack() and grid() inside the same container. Try it and Tkinter stops you with a clear error, TclError: cannot use geometry manager grid inside . which already has slaves managed by pack. Pick one manager per container and stick with it.
Build: Calculator Application
This is the destination of this Python Tkinter tutorial. A small calculator window: a display across the top, a grid of buttons below, basic arithmetic, a clear button, and keyboard support. Here is exactly what it should do when it is finished:
- Show a right aligned display that starts at
0. - Add, subtract, multiply, divide, and group with parentheses.
- Calculate when you press
=or the Enter key. - Clear with the
Cbutton or the Escape key. - Show
Errorinstead of crashing on bad input like dividing by zero.
We wrap the whole thing in a class. That is not ceremony for its own sake. A GUI has state (the current expression) and behavior (what each button does), and a class is the natural home for both. The display, the buttons, and the keyboard bindings each get their own small method, so the constructor reads like a table of contents.
📄 calculator.py: the complete Tkinter calculator
import tkinter as tk
class Calculator:
def __init__(self):
self.window = tk.Tk()
self.window.title("Python Calculator")
self.window.resizable(False, False)
self.expression = ""
self.display_var = tk.StringVar(value="0")
self._create_display()
self._create_buttons()
self._bind_keys()
def _create_display(self):
display = tk.Entry(
self.window,
textvariable=self.display_var,
font=("Consolas", 24),
justify="right",
bd=5,
state="readonly",
)
display.grid(row=0, column=0, columnspan=4, padx=5, pady=5, sticky="ew")
def _create_buttons(self):
buttons = [
("C", 1, 0), ("(", 1, 1), (")", 1, 2), ("/", 1, 3),
("7", 2, 0), ("8", 2, 1), ("9", 2, 2), ("*", 2, 3),
("4", 3, 0), ("5", 3, 1), ("6", 3, 2), ("-", 3, 3),
("1", 4, 0), ("2", 4, 1), ("3", 4, 2), ("+", 4, 3),
("0", 5, 0, 2), (".", 5, 2), ("=", 5, 3),
]
for item in buttons:
text, row, col = item[0], item[1], item[2]
colspan = item[3] if len(item) > 3 else 1
btn = tk.Button(
self.window,
text=text,
font=("Arial", 18),
width=5,
height=2,
command=lambda t=text: self._on_button(t),
)
btn.grid(row=row, column=col, columnspan=colspan, padx=2, pady=2, sticky="ew")
def _on_button(self, char):
if char == "C":
self.expression = ""
self.display_var.set("0")
elif char == "=":
try:
result = str(eval(self.expression)) # eval is a shortcut here, see the warning below
self.display_var.set(result)
self.expression = result
except Exception:
self.display_var.set("Error")
self.expression = ""
else:
self.expression += char
self.display_var.set(self.expression)
def _bind_keys(self):
self.window.bind("", lambda e: self._on_button("="))
self.window.bind("", lambda e: self._on_button("C"))
for key in "0123456789+-*/.()":
self.window.bind(key, lambda e, k=key: self._on_button(k))
def run(self):
self.window.mainloop()
if __name__ == "__main__":
Calculator().run()
Run it with python calculator.py and the window appears. Click the buttons or just type. Press Enter to calculate, Escape to clear. To prove the math is right, we drove the buttons in code and read the display back. Here is what shows up:
▶ Display after each key sequence
Type 7 + 8 then = -> 15 Press C -> 0 Type ( 2 + 3 ) * 4 = -> 20 Type 1 0 / 0 = -> Error
What happened here: The whole calculator runs on one idea. A string called self.expression collects everything you press, and a StringVar mirrors it onto the display. Press a digit or operator and we tack it onto the string. Press = and we hand that string to eval(), turn the answer back into text, and show it. Press C and we wipe the string back to 0. The try and except around eval() is what saves us: feed it 10/0 and Python raises an error, we catch it, and the display calmly reads Error instead of the program crashing.
Notice the lambda t=text: in the button loop. That t=text is not optional. It captures the current button’s character right now, so every button remembers its own value. Leave it out and all nineteen buttons end up sending the last character in the list, which is the exact bug we dissect in the next section.
eval(): it works here because the only thing that ever reaches it is the small set of digits and operators on the buttons. But eval() runs any Python you give it, so it is genuinely dangerous the moment untrusted text can reach it. Never point eval() at something a stranger typed. For real arithmetic input, parse the expression yourself by walking the Python ast module and allowing only number and operator nodes, or use a small math expression parser. (Note that ast.literal_eval is safe but only reads plain literals like 42; it rejects 2+3, so it is not a drop in replacement here.) We use eval() purely to keep the calculator short and focused on the GUI.Common Mistakes
Mistake 1: Lambda in a loop captures the variable, not its value
This is the single most common Tkinter bug, and it bites everyone once. You build buttons in a loop, give each a lambda, and every button does the wrong thing. The lambda does not remember the value of i at the moment it was created. It remembers the name i, and by the time anyone clicks, the loop has finished and i is stuck on its final value. It is like leaving a note that says “pay whatever the petrol price is” instead of writing down today’s price: by the time the note is read, the price has changed.
❌ Wrong: the lambda reads i later, after the loop ended
# Every button ends up printing the LAST value of i
buttons = []
for i in range(3):
b = tk.Button(root, text=str(i), command=lambda: print(i))
buttons.append(b)
# Click button 0, then 1, then 2:
for b in buttons:
b.invoke()
▶ Output
2 2 2
✅ Fix: a default argument captures the value right now
buttons = []
for i in range(3):
b = tk.Button(root, text=str(i), command=lambda x=i: print(x))
buttons.append(b)
for b in buttons:
b.invoke()
▶ Output
0 1 2
Why: default argument values are evaluated once, when the lambda is defined, not when it runs. So lambda x=i: print(x) grabs the current i and freezes it into x. The buggy version has no default, so it looks up i only when clicked, long after the loop set it to 2. This is the exact reason the calculator uses command=lambda t=text: self._on_button(t) instead of command=lambda: self._on_button(text).
Mistake 2: Calling a function instead of passing it
The command option wants a function to call later, not the result of calling it now. Add parentheses by accident and the function runs the instant the window builds, then never again.
❌ Wrong: greet() runs immediately, the button does nothing
tk.Button(root, text="Greet", command=greet()) # called right away
✅ Correct: pass the function itself, no parentheses
tk.Button(root, text="Greet", command=greet) # called on click
Why: greet is the function object. greet() is what you get back after running it, usually None. Hand Tkinter greet() and you are really setting command=None, so clicking does nothing. When you need to pass arguments, say greeting a user named Anvi, wrap the call in a lambda: command=lambda: greet("Anvi").
What Could Go Wrong
Four problems catch almost everyone the first time. Here they are with the fix.
- Window flashes open then closes. You forgot
root.mainloop(), or yourcommandhas parentheses (command=greet()instead ofcommand=greet). Addmainloop()and drop the parentheses. TclError: cannot use geometry manager grid inside . which already has slaves managed by pack. You mixedpack()andgrid()in the same container. Pick one per container.- All buttons do the same thing. A lambda in a loop with no default argument. Use
lambda x=i: ...to capture the current value, exactly like the calculator does. _tkinter.TclError: no display name and no $DISPLAY environment variable. You are on a headless Linux box or container with no screen. Tkinter needs a real display. Run it on a desktop, or use a virtual framebuffer likexvfb.
Try It Yourself
The calculator is a great sandbox. Pick one of these and build it yourself. No step by step, just the idea, because figuring out the wiring is where the learning lives.
- Add a backspace button (and bind it to the Backspace key) that deletes the last character of the expression.
- Add a small history panel that shows the last five calculations above the display.
- Add a dark mode toggle that swaps the background and text colors of every widget.
- Replace
eval()with a tiny safe parser so the calculator can never run arbitrary code.
Conclusion
You went from an empty script to a real Python Tkinter desktop app. You created a root window, added labels, entries, comboboxes, and buttons, arranged them with pack and grid, and wired events to callback functions. Then you pulled it all together in a calculator class with a StringVar backed display, a button grid built in a loop, and keyboard bindings, dodging the two classic traps on the way: the lambda-in-a-loop capture bug and passing greet() instead of greet.
Next up is packaging your project with pyproject.toml and Poetry, so tools like this calculator can be installed and shared like real software. And if you want to jump around or revisit earlier topics, browse the full Python + AI/ML tutorial series home.
Frequently Asked Questions
What is Tkinter in Python?
Tkinter is Python’s built-in GUI toolkit, and it is the starting point of any Python Tkinter tutorial. It gives you widgets (buttons, labels, text fields), layout managers (pack, grid, place), and event handling to build real desktop apps. It ships with Python, so there is no pip install.
Why does my Tkinter window open and close instantly?
You almost certainly forgot to call root.mainloop() at the end, or you called a function with parentheses in a command instead of passing it. mainloop() is the line that keeps the window alive and listening for clicks. Without it, the script reaches the end and exits, so the window vanishes.
Is Tkinter good for production apps?
For internal tools, utilities, and simple desktop apps, yes, Python Tkinter holds up fine. For polished commercial software, look at PyQt6, PySide6, or Kivy. Tkinter looks dated by default, but ttk themed widgets and libraries like customtkinter clean it up a lot.
How do I make Tkinter look modern?
Use ttk themed widgets instead of the plain tk ones, and reach for ttkthemes or customtkinter for modern dark and light looks that match the operating system. Even just switching buttons and entries to their ttk versions is a noticeable upgrade.
Can a Tkinter app talk to a database?
Yes. Tkinter handles the window and widgets, and your own Python code handles the data using sqlite3, SQLAlchemy, or any database library. Keep the GUI code and the data code in separate classes so each stays small and testable.
Interview Questions on Python Tkinter
The same ideas as they show up in real interviews, framed as scenarios you can practice out loud.
Q: A user clicks your “Download report” button. The download takes 20 seconds, and the whole window goes gray and shows “Not Responding” until it finishes. Why, and what is the fix?
Tkinter runs everything on one thread inside mainloop(). While your callback is busy downloading, the loop cannot process redraws or clicks, so the operating system marks the window as unresponsive. The fix is to run the slow work in a threading.Thread, have it push its result into a queue.Queue, and poll that queue from the GUI side with root.after(100, check_queue). The worker thread must never create or update widgets directly; only the thread running mainloop() should touch Tkinter objects.
Q: What exactly does root.mainloop() do, and when does the line after it run?
It starts Tk’s event loop: an endless cycle that waits for events like clicks, key presses, and redraw requests, matches each event to a widget’s command or binding, and calls your callback. The call blocks, so the line after mainloop() runs only after the window is destroyed, either by the user closing it or by code calling root.destroy(). That is why it is always the last line of a Tkinter script.
Q: You move working image-label code into a function, and now the label shows up blank with no error. What happened?
The PhotoImage object was a local variable, and the Label widget does not keep a Python reference to it. When the function returns, Python garbage collects the image, and Tk quietly draws nothing. The classic fix is to keep a reference alive yourself: label.image = photo right after creating the label, or store the image on self in a class. This is one of the most famous catches in Tkinter.
Q: Why does Tkinter refuse to mix pack() and grid() in the same container, and how do you still use both in one app?
Both managers negotiate the container’s size with its children, so if both ran at once, each would keep undoing the other’s layout in an endless tug of war. Tkinter detects this and raises a TclError instead of freezing. The rule is one manager per container, not per app: you can pack two frames into the root, then use grid for the widgets inside one frame and pack inside the other.
Q: What do Tkinter variable classes like StringVar and BooleanVar give you over setting widget text directly?
They give you two-way binding: calling set() updates every widget attached through textvariable, and get() reads the current value even after the user edits it. They also work on widgets the user cannot type into, which is how the calculator updates its readonly display. On top of that, trace_add("write", callback) runs a function every time the value changes, which is the clean way to build live validation or a character counter.
Q: The calculator uses eval() on the expression string. How would you make it safe for untrusted input?
eval() executes arbitrary Python, so any path that lets free text reach it is a code execution hole. The safe approach is to parse the string with ast.parse(expr, mode="eval"), walk the tree, allow only number constants and arithmetic operator nodes, and reject everything else before computing the result yourself. Note that ast.literal_eval alone is not enough: it is safe but only accepts plain literals, so it rejects 2+3. A small expression parser library is also a reasonable answer.
Go deeper: the official Python documentation covers every edge case of this topic.
Related Posts
Previous: Python: Command-Line Interface (CLI) Tools with argparse and click
Next: Python: Packaging Your Project (pyproject.toml, Poetry)
Series Home: Python + AI/ML Tutorial Series

No comment