Beginner

You have written Python scripts that run in the terminal — but what if you want a real window with buttons, text fields, and menus? Python ships with tkinter, a fully capable GUI toolkit that lets you build desktop applications without installing anything. Whether you want a quick utility tool, an admin interface for your scripts, or your first ever desktop app, tkinter is the fastest path from idea to a working window on your screen.

tkinter is part of the Python standard library on Windows and macOS. On Linux you may need to install the python3-tk package via your package manager (sudo apt install python3-tk). Once that is done, you need nothing else — no pip, no virtual environments, no framework setup.

This tutorial covers everything you need to know to build real desktop apps: the widget system, geometry managers (pack, grid, place), event handling, entry forms, listboxes, message dialogs, and the main event loop. You will finish by building a task manager GUI you can use every day.

Your First tkinter Window: Quick Example

Here is the minimum viable tkinter app — a window with a label and a button that changes text when clicked:

# hello_tkinter.py
import tkinter as tk

def on_click():
    label.config(text="You clicked it!")

root = tk.Tk()
root.title("My First App")
root.geometry("300x150")   # width x height in pixels

label = tk.Label(root, text="Hello, tkinter!", font=("Arial", 14))
label.pack(pady=20)

button = tk.Button(root, text="Click Me", command=on_click)
button.pack()

root.mainloop()   # starts the event loop -- blocks until window closes

Output: A 300×150 window appears with a label and button. Clicking the button changes the label text to “You clicked it!”

Every tkinter app has the same three-part structure: create the root window with tk.Tk(), add widgets and bind callbacks, then call root.mainloop() to start the event loop. The event loop listens for mouse clicks, keyboard presses, and other user actions and dispatches them to your callback functions. The sections below show you how to build complex layouts with this same pattern.

What Is tkinter and When Should You Use It?

tkinter is Python’s wrapper around the Tk GUI toolkit, which originated in the Tcl programming language. It provides a set of widgets (visual components) and a geometry manager (layout system) that together let you describe how your interface looks and behaves in pure Python.

LibraryBest ForInstall RequiredLook and Feel
tkinterSimple utilities, quick tools, learningNo (stdlib)Native on each OS
PyQt / PySideProfessional commercial appsYesConsistent, polished
wxPythonWindows-heavy desktop appsYesVery native Windows
Dear PyGuiData dashboards, GPU-acceleratedYesCustom dark-mode

tkinter shines for internal tools, quick prototypes, and educational projects where “works everywhere with no install” matters more than pixel-perfect polish. For a commercial product with a design team, PyQt or PySide are better choices. But for the vast majority of Python developer use cases — wrapping a script in a GUI, building a team utility, or learning GUI fundamentals — tkinter is exactly the right tool.

Core Widgets

tkinter provides over 20 widget types. These are the ones you will use in almost every app:

# core_widgets.py
import tkinter as tk
from tkinter import ttk   # themed widgets (look more modern)

root = tk.Tk()
root.title("Widget Gallery")
root.geometry("400x500")

# Label -- display text or images
tk.Label(root, text="I am a Label", font=("Arial", 12)).pack(pady=5)

# Button -- triggers a callback
tk.Button(root, text="I am a Button", bg="#4CAF50", fg="white").pack(pady=5)

# Entry -- single-line text input
entry = tk.Entry(root, width=30)
entry.insert(0, "Type something here")
entry.pack(pady=5)

# Text -- multi-line text area
text = tk.Text(root, width=40, height=4)
text.insert("1.0", "Multi-line\ntext widget")
text.pack(pady=5)

# Checkbutton -- boolean toggle
var = tk.BooleanVar(value=True)
tk.Checkbutton(root, text="Enable feature", variable=var).pack(pady=5)

# Radiobutton -- choose one from many
choice = tk.StringVar(value="option1")
for opt in ["option1", "option2", "option3"]:
    tk.Radiobutton(root, text=opt, variable=choice, value=opt).pack()

# Combobox (ttk) -- dropdown menu
combo = ttk.Combobox(root, values=["Python", "Go", "Rust", "TypeScript"])
combo.set("Python")
combo.pack(pady=5)

# Listbox -- scrollable list of items
lb = tk.Listbox(root, height=3)
for item in ["Item A", "Item B", "Item C"]:
    lb.insert(tk.END, item)
lb.pack(pady=5)

root.mainloop()

Output: A window displaying each widget type, all functioning and interactive.

The ttk submodule provides themed versions of many widgets that look more native on modern operating systems. Use ttk.Combobox, ttk.Treeview, and ttk.Progressbar where available — they look noticeably more polished than their tk counterparts, especially on Windows.

Layout Management

tkinter has three geometry managers for positioning widgets. You must use the same manager for all widgets inside a given container — mixing pack and grid in the same frame will cause unpredictable behaviour.

# layout_grid.py
import tkinter as tk

root = tk.Tk()
root.title("Grid Layout")
root.geometry("300x200")

# grid() arranges widgets in rows and columns
tk.Label(root, text="Name:").grid(row=0, column=0, sticky="w", padx=10, pady=8)
tk.Entry(root, width=20).grid(row=0, column=1, padx=10, pady=8)

tk.Label(root, text="Email:").grid(row=1, column=0, sticky="w", padx=10, pady=8)
tk.Entry(root, width=20).grid(row=1, column=1, padx=10, pady=8)

tk.Label(root, text="Role:").grid(row=2, column=0, sticky="w", padx=10, pady=8)
tk.Entry(root, width=20).grid(row=2, column=1, padx=10, pady=8)

# columnspan stretches a widget across multiple columns
tk.Button(root, text="Submit", width=20).grid(
    row=3, column=0, columnspan=2, pady=15
)

root.mainloop()

Output: A clean form layout with labels and entry fields aligned in two columns and a full-width submit button.

sticky controls alignment within the grid cell — "w" means align to the left (west), "ew" means stretch to fill the full width. padx and pady add spacing around the widget. For most forms and dialogs, grid() is the clearest layout manager because row and column positions are explicit and predictable.

Event Handling

Beyond button clicks, tkinter can respond to keyboard input, mouse movement, window resize, and more. You bind event listeners to widgets using the bind() method:

# event_handling.py
import tkinter as tk

root = tk.Tk()
root.title("Event Demo")
root.geometry("350x200")

output = tk.Label(root, text="Press a key or click", font=("Arial", 12))
output.pack(pady=30)

# Keyboard event -- fires when any key is pressed
def on_key(event):
    output.config(text=f"Key pressed: '{event.char}' (keycode {event.keycode})")

# Mouse click event
def on_click(event):
    output.config(text=f"Clicked at ({event.x}, {event.y})")

# Double-click event
def on_double(event):
    output.config(text="Double-clicked!")

root.bind("", on_key)
root.bind("", on_click)       # left mouse button
root.bind("", on_double)

# Entry validation: block non-numeric input
validate_cmd = root.register(lambda s: s.isdigit() or s == '')
num_entry = tk.Entry(root, validate="key",
                     validatecommand=(validate_cmd, '%S'), width=15)
num_entry.pack(pady=10)
tk.Label(root, text="(numbers only entry above)").pack()

root.mainloop()

Output: A window that displays which key was pressed or where the mouse clicked. The entry field rejects any non-digit input as you type.

Event strings follow Tk’s binding syntax: "<KeyPress>", "<Button-1>", "<Return>", "<Configure>" (window resize), "<FocusIn>". The event object passed to callbacks contains context-specific attributes like event.char, event.keycode, event.x, and event.y.

Message Boxes and File Dialogs

tkinter’s messagebox and filedialog submodules give you native OS dialogs for confirmations, errors, and file selection without writing a single layout line:

# dialogs.py
import tkinter as tk
from tkinter import messagebox, filedialog, simpledialog

root = tk.Tk()
root.title("Dialogs Demo")
root.geometry("300x200")

def show_info():
    messagebox.showinfo("Done", "Your file has been saved successfully.")

def ask_confirm():
    result = messagebox.askyesno("Confirm", "Delete selected items?")
    status.config(text=f"You chose: {'Yes' if result else 'No'}")

def pick_file():
    path = filedialog.askopenfilename(
        title="Select a file",
        filetypes=[("Text files", "*.txt"), ("All files", "*.*")]
    )
    status.config(text=f"Selected: {path}" if path else "Cancelled")

def ask_name():
    name = simpledialog.askstring("Input", "Enter your name:")
    if name:
        status.config(text=f"Hello, {name}!")

tk.Button(root, text="Show Info",   command=show_info,   width=20).pack(pady=5)
tk.Button(root, text="Ask Yes/No",  command=ask_confirm, width=20).pack(pady=5)
tk.Button(root, text="Pick File",   command=pick_file,   width=20).pack(pady=5)
tk.Button(root, text="Ask Name",    command=ask_name,    width=20).pack(pady=5)

status = tk.Label(root, text="", wraplength=280)
status.pack(pady=10)

root.mainloop()

Output: Each button opens a native OS dialog. The selected result is displayed in the label below the buttons.

These dialogs block execution until the user responds — askyesno() returns True or False, askopenfilename() returns a file path string (or empty string if cancelled). Using native dialogs means your app automatically matches the operating system’s look and feel without any custom CSS or theming work.

Real-Life Example: Task Manager App

Let us build a practical task manager with a full CRUD interface — add tasks, mark them complete, and delete them — all in one tkinter window:

# task_manager.py
import tkinter as tk
from tkinter import messagebox, simpledialog

class TaskManagerApp:
    def __init__(self, root):
        self.root = root
        self.root.title("Task Manager")
        self.root.geometry("480x400")
        self.root.resizable(True, True)
        self.tasks = []   # list of {'text': str, 'done': bool}
        self._build_ui()

    def _build_ui(self):
        # Top frame: entry + add button
        top = tk.Frame(self.root)
        top.pack(fill="x", padx=10, pady=8)

        self.task_var = tk.StringVar()
        entry = tk.Entry(top, textvariable=self.task_var, font=("Arial", 12))
        entry.pack(side="left", fill="x", expand=True, padx=(0, 8))
        entry.bind("", lambda e: self.add_task())

        tk.Button(top, text="Add", width=8, command=self.add_task,
                  bg="#4CAF50", fg="white").pack(side="left")

        # Middle frame: listbox with scrollbar
        mid = tk.Frame(self.root)
        mid.pack(fill="both", expand=True, padx=10, pady=4)

        scrollbar = tk.Scrollbar(mid)
        scrollbar.pack(side="right", fill="y")

        self.listbox = tk.Listbox(mid, font=("Arial", 12),
                                  yscrollcommand=scrollbar.set,
                                  selectmode="single", activestyle="none")
        self.listbox.pack(fill="both", expand=True)
        scrollbar.config(command=self.listbox.yview)

        # Bottom frame: action buttons
        bot = tk.Frame(self.root)
        bot.pack(fill="x", padx=10, pady=8)

        tk.Button(bot, text="Mark Done",  command=self.mark_done,
                  width=12, bg="#2196F3", fg="white").pack(side="left", padx=4)
        tk.Button(bot, text="Edit",       command=self.edit_task,
                  width=12).pack(side="left", padx=4)
        tk.Button(bot, text="Delete",     command=self.delete_task,
                  width=12, bg="#f44336", fg="white").pack(side="left", padx=4)

        self.count_label = tk.Label(bot, text="0 tasks")
        self.count_label.pack(side="right", padx=4)

    def _refresh(self):
        self.listbox.delete(0, tk.END)
        for task in self.tasks:
            prefix = "[x] " if task['done'] else "[ ] "
            self.listbox.insert(tk.END, prefix + task['text'])
        total = len(self.tasks)
        done  = sum(1 for t in self.tasks if t['done'])
        self.count_label.config(text=f"{done}/{total} done")

    def add_task(self):
        text = self.task_var.get().strip()
        if not text:
            return
        self.tasks.append({'text': text, 'done': False})
        self.task_var.set("")
        self._refresh()

    def mark_done(self):
        sel = self.listbox.curselection()
        if not sel:
            messagebox.showwarning("No selection", "Please select a task first.")
            return
        idx = sel[0]
        self.tasks[idx]['done'] = not self.tasks[idx]['done']
        self._refresh()

    def edit_task(self):
        sel = self.listbox.curselection()
        if not sel:
            messagebox.showwarning("No selection", "Please select a task first.")
            return
        idx = sel[0]
        new_text = simpledialog.askstring(
            "Edit Task", "Update task text:",
            initialvalue=self.tasks[idx]['text']
        )
        if new_text:
            self.tasks[idx]['text'] = new_text.strip()
            self._refresh()

    def delete_task(self):
        sel = self.listbox.curselection()
        if not sel:
            messagebox.showwarning("No selection", "Please select a task first.")
            return
        idx = sel[0]
        task_text = self.tasks[idx]['text']
        if messagebox.askyesno("Delete", f"Delete '{task_text}'?"):
            self.tasks.pop(idx)
            self._refresh()

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

Output: A fully functional task manager window. Add tasks with the entry field, mark them done (toggles [x]), edit task text, or delete with a confirmation dialog. The counter in the bottom-right tracks completed vs total tasks.

This class-based structure is the recommended pattern for real apps — it keeps state (self.tasks), layout (_build_ui), and update logic (_refresh) cleanly separated. You can extend it by adding persistent storage with json or sqlite3, due-date columns using ttk.Treeview, or keyboard shortcuts with additional root.bind() calls.

Frequently Asked Questions

Why does my program freeze when I call a long function from a button?

tkinter is single-threaded. If your callback does anything slow (network request, file processing, heavy calculation), it blocks the event loop and the window appears frozen. The fix is to run the slow work in a background thread using Python’s threading.Thread, then use root.after(0, update_ui_function) to push the result back to the main thread safely. Never update tkinter widgets from a background thread directly — only from the main thread.

Can I use pack() and grid() in the same window?

Not in the same container — mixing them inside the same frame or window will cause a TclError. The workaround is to use nested Frame widgets: the outer frame uses pack() to position major sections (header, body, footer), and each inner frame uses grid() for its own contents. This separation keeps your layout code clean and avoids the constraint conflict.

How do I make tkinter look more modern?

Use the ttk module instead of basic tk widgets where available, and apply a theme with style = ttk.Style(); style.theme_use('clam') (available themes vary by OS). The ttkthemes third-party package provides polished options like arc and equilux. For a fully custom look, configure widget colors and fonts using widget.config(bg='#2b2b2b', fg='#ffffff').

How do I display images in tkinter?

Use the PIL (Pillow) library: from PIL import Image, ImageTk. Open the image, convert it to a PhotoImage with ImageTk.PhotoImage(img), and set it on a Label with label.config(image=photo). Keep a reference to the PhotoImage object (e.g., label.image = photo) — Python’s garbage collector will delete it and your image will disappear without this step.

How do I package a tkinter app for distribution?

Use PyInstaller to bundle your app into a standalone executable: pyinstaller --onefile --windowed myapp.py. The --windowed flag prevents a terminal window from opening alongside the GUI on Windows. For macOS, py2app creates a proper .app bundle. Both tools include the Python interpreter and all dependencies, so end users do not need Python installed.

Conclusion

Python’s tkinter module puts a complete GUI toolkit at your fingertips with zero installation overhead. In this tutorial, you worked with the core widgets (Label, Button, Entry, Text, Listbox, Checkbutton, Combobox), organised layouts with grid() and pack(), handled keyboard and mouse events with bind(), used native OS dialogs from messagebox and filedialog, and built a full task manager with class-based architecture.

The task manager is a solid foundation to extend — add file persistence, drag-and-drop reordering, or a priority column to make it genuinely useful in your daily workflow. tkinter’s simplicity is its superpower: you can prototype and ship a GUI utility in an afternoon without learning a web framework or a complex GUI toolkit.

Explore the full widget reference and geometry manager documentation in the official Python docs: tkinter — Python interface to Tcl/Tk.