Python Tkinter: Build Desktop GUIs from Scratch

Tkinter is Python's built-in library for creating graphical user interfaces. It ships with every standard Python installation, requires no extra downloads, and gives you everything you need to build functional desktop applications with buttons, text fields, menus, and more. This guide walks through the core concepts of Tkinter so you can start building your own GUI programs right away.

If you have ever wanted to give your Python scripts a visual interface instead of running them in a terminal, Tkinter is the natural place to start. It is lightweight, well-documented, and powerful enough to handle everything from quick utility tools to full-featured desktop applications. By the end of this guide, you will understand how Tkinter's widget system works, how to arrange elements on screen, how to respond to user input, and how to use the modern themed widget set for a polished look.

What Is Tkinter and Why Use It

Tkinter stands for "Tk interface." It is Python's binding to the Tcl/Tk GUI toolkit, which has been around since the early 1990s. Tk itself is a cross-platform widget library that works on Windows, macOS, and Linux. Because Tkinter is part of the Python standard library, you do not need to install anything extra. If Python is on your machine, Tkinter is already there.

The official Python binary release bundles Tcl/Tk 8.6, which includes the themed widget set (ttk) that gives your applications a native look and feel on each operating system. On Linux, you may need to install a separate package depending on your distribution. Ubuntu users, for example, can install it with sudo apt install python3-tk.

Note

You can verify that Tkinter is installed and check which version of Tcl/Tk you are running by executing python -m tkinter from the command line. A small demo window will appear showing the version number.

Tkinter is a strong choice for beginners and for projects that need a straightforward GUI without heavy external dependencies. It comes pre-installed, it is portable across operating systems, and it requires very little boilerplate code to get a window on screen. For complex, highly styled applications or mobile development, other frameworks like PySide6 or Kivy may be better suited, but for standard desktop tools and utilities, Tkinter remains one of the simplest paths from idea to working application.

Creating Your First Window

Every Tkinter application starts by creating a root window. This is the main application window that holds all of your widgets. The pattern is the same every time: import tkinter, create a Tk instance, configure it, and then call mainloop() to start the event loop.

import tkinter as tk

# Create the root window
root = tk.Tk()
root.title("My First App")
root.geometry("400x300")

# Start the event loop
root.mainloop()

The title() method sets the text that appears in the window's title bar. The geometry() method accepts a string in the format "widthxheight" measured in pixels. Once mainloop() is called, Tkinter enters an infinite loop that listens for events like mouse clicks and keyboard presses, updates the display, and keeps the window responsive.

Without mainloop(), the window would flash on screen and immediately close because the script would reach its end and exit. This event loop is the heartbeat of every Tkinter application. It only stops when the user closes the window or when you explicitly call root.destroy().

Pro Tip

You can prevent users from resizing the window with root.resizable(False, False). The two boolean arguments control horizontal and vertical resizing independently.

Essential Widgets

Widgets are the building blocks of a Tkinter interface. Every element you see in a GUI application -- buttons, labels, text fields, checkboxes -- is a widget. Each widget is a Python object created from a class, and it must be assigned to a parent widget (usually the root window or a frame).

Label

The Label widget displays text or an image. It is read-only from the user's perspective, making it ideal for headings, instructions, and status messages.

label = tk.Label(root, text="Welcome to Tkinter", font=("Arial", 16))
label.pack(pady=10)

Button

Buttons trigger actions when clicked. You connect a button to a function using the command parameter.

def on_click():
    label.config(text="Button was clicked!")

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

Entry

The Entry widget creates a single-line text input field. To read the current value, call the get() method on the widget. You can also link it to a StringVar to watch for changes automatically.

name_var = tk.StringVar()
entry = tk.Entry(root, textvariable=name_var, width=30)
entry.pack(pady=5)

# Later, retrieve the value
user_input = name_var.get()

Text

For multi-line text input, use the Text widget. Unlike Entry, it supports multiple lines and basic text formatting. Retrieving its contents requires specifying a range using Tkinter's index notation.

text_box = tk.Text(root, height=5, width=40)
text_box.pack(pady=5)

# Get all content from the Text widget
content = text_box.get("1.0", tk.END)

The index "1.0" means line 1, character 0 (the very beginning). tk.END represents the position after the last character.

Checkbutton and Radiobutton

Checkbuttons allow users to toggle an option on or off. Radiobuttons let users select exactly one option from a group. Both use a variable to track their current state.

# Checkbutton
agree_var = tk.BooleanVar()
check = tk.Checkbutton(root, text="I agree", variable=agree_var)
check.pack()

# Radiobuttons
choice_var = tk.StringVar(value="option1")
radio1 = tk.Radiobutton(root, text="Option 1", variable=choice_var, value="option1")
radio2 = tk.Radiobutton(root, text="Option 2", variable=choice_var, value="option2")
radio1.pack()
radio2.pack()

Frame

The Frame widget is an invisible container used to group and organize other widgets. Frames are essential for building complex layouts because you can nest them inside each other and apply different geometry managers to each one.

toolbar = tk.Frame(root, bg="#333333", height=40)
toolbar.pack(fill=tk.X)

content_area = tk.Frame(root)
content_area.pack(fill=tk.BOTH, expand=True)

Geometry Managers: Laying Out Your Interface

Creating widgets is only half the job. You also need to tell Tkinter where to place them on screen. Tkinter provides three geometry managers for this purpose: pack, grid, and place. Each takes a different approach to positioning, and you choose the one that fits your layout needs.

Warning

Never mix pack and grid within the same parent container. Tkinter will enter an infinite loop trying to reconcile the two layout strategies. Use one geometry manager per container, but you can use different managers in different frames.

pack

The pack manager places widgets one after another in a single direction. By default, widgets stack from top to bottom. You can change this using the side parameter with values like tk.LEFT, tk.RIGHT, tk.TOP, or tk.BOTTOM.

button1 = tk.Button(root, text="Left")
button1.pack(side=tk.LEFT, padx=5)

button2 = tk.Button(root, text="Right")
button2.pack(side=tk.RIGHT, padx=5)

The fill parameter controls whether a widget stretches to fill available space (tk.X for horizontal, tk.Y for vertical, tk.BOTH for both directions). The expand parameter, when set to True, tells the widget to claim any extra space in the parent container.

grid

The grid manager arranges widgets in a table-like structure of rows and columns. It is the recommended choice for form layouts and any interface where elements need to align both horizontally and vertically.

tk.Label(root, text="Username:").grid(row=0, column=0, sticky=tk.W, padx=5, pady=5)
tk.Entry(root).grid(row=0, column=1, padx=5, pady=5)

tk.Label(root, text="Password:").grid(row=1, column=0, sticky=tk.W, padx=5, pady=5)
tk.Entry(root, show="*").grid(row=1, column=1, padx=5, pady=5)

tk.Button(root, text="Login").grid(row=2, column=1, sticky=tk.E, padx=5, pady=10)

The sticky parameter works like a compass. It accepts the values tk.N, tk.S, tk.E, and tk.W (north, south, east, west) and controls how a widget attaches to the edges of its cell. Setting sticky=tk.EW stretches a widget to fill the cell horizontally. Use columnspan and rowspan to make a widget occupy multiple cells.

To make columns and rows resize proportionally when the window is resized, use columnconfigure() and rowconfigure() with a weight value.

root.columnconfigure(1, weight=1)
root.rowconfigure(0, weight=1)

place

The place manager gives you absolute control by letting you specify exact pixel coordinates or relative positions. While powerful, it does not adapt well to window resizing, so it is best reserved for special cases like overlays or custom-positioned elements.

label = tk.Label(root, text="Floating Label")
label.place(x=50, y=100)

# Or use relative positioning (0.0 to 1.0)
label.place(relx=0.5, rely=0.5, anchor=tk.CENTER)

Event Handling and Callbacks

Tkinter applications are event-driven. Instead of running code in a straight line from top to bottom, the program waits for events (mouse clicks, key presses, window resizes) and responds to them by calling functions you have defined.

The simplest way to handle events is through the command parameter on widgets like Button. You pass it a function reference (without parentheses), and Tkinter calls that function when the button is clicked.

def greet():
    print("Hello!")

button = tk.Button(root, text="Say Hello", command=greet)
button.pack()

For more flexibility, use the bind() method. It lets you attach a callback function to any event on any widget. The callback receives an event object containing details about what happened.

def on_key_press(event):
    print(f"You pressed: {event.char}")

root.bind("<Key>", on_key_press)

Common event patterns include <Button-1> for a left mouse click, <Return> for the Enter key, <FocusIn> and <FocusOut> for focus changes, and <Configure> for window resize events. The event object provides attributes like event.x and event.y for mouse coordinates, event.char for the character pressed, and event.widget for the widget that received the event.

Pro Tip

If your callback function needs to accept arguments beyond the event object, use a lambda as a wrapper: button.bind("<Button-1>", lambda e: my_func(e, extra_arg)).

The after() Method for Scheduling

Sometimes you need to run code after a delay or repeat an action at intervals. The after() method schedules a function call after a specified number of milliseconds without blocking the event loop.

import time

def update_clock():
    current_time = time.strftime("%H:%M:%S")
    clock_label.config(text=current_time)
    root.after(1000, update_clock)  # Call again in 1 second

update_clock()

This is the correct way to run recurring tasks in Tkinter. Never use time.sleep() inside a Tkinter application because it blocks the event loop and freezes the entire interface.

Themed Widgets with ttk

The tkinter.ttk module provides a set of themed widgets that automatically adopt the native appearance of whatever operating system the application is running on. On Windows, ttk widgets look like standard Windows controls. On macOS, they match the macOS style. This addresses a long-standing criticism of Tkinter's older widgets, which had a distinctly dated appearance.

import tkinter as tk
from tkinter import ttk

root = tk.Tk()

# ttk widgets use the native platform style
label = ttk.Label(root, text="Themed Label")
label.pack(padx=10, pady=5)

button = ttk.Button(root, text="Themed Button")
button.pack(padx=10, pady=5)

entry = ttk.Entry(root, width=30)
entry.pack(padx=10, pady=5)

root.mainloop()

The ttk module includes 18 widgets in total. Twelve of them are themed replacements for existing classic widgets: Button, Checkbutton, Entry, Frame, Label, LabelFrame, Menubutton, PanedWindow, Radiobutton, Scale, Scrollbar, and Spinbox. The remaining six are widgets that only exist in ttk: Combobox, Notebook, Progressbar, Separator, Sizegrip, and Treeview.

Note

One key difference between classic and ttk widgets is how you apply styles. Classic widgets use options like fg and bg directly. In ttk, appearance is controlled through a separate Style system, which separates behavior from visual presentation.

Styling ttk Widgets

To customize the look of ttk widgets, you create and configure styles using the ttk.Style class.

style = ttk.Style()
style.configure("Custom.TButton", padding=6, relief="flat", background="#4b8bbe")

button = ttk.Button(root, text="Styled Button", style="Custom.TButton")
button.pack()

Style names follow a convention. The built-in style for a button is "TButton", for a label it is "TLabel", and so on. When you create a custom style, prefix it with a name of your choice followed by a dot, like "Custom.TButton".

Combobox, Notebook, and Treeview

Three of the ttk-exclusive widgets deserve special mention because they solve common interface needs.

The Combobox provides a dropdown list that lets users select from predefined options or type a custom value.

combo = ttk.Combobox(root, values=["Python", "JavaScript", "Rust", "Go"])
combo.set("Python")
combo.pack(pady=5)

The Notebook widget creates a tabbed interface, allowing you to switch between different pages of content within the same window.

notebook = ttk.Notebook(root)

tab1 = ttk.Frame(notebook)
tab2 = ttk.Frame(notebook)

notebook.add(tab1, text="General")
notebook.add(tab2, text="Settings")
notebook.pack(fill=tk.BOTH, expand=True)

The Treeview widget displays hierarchical or tabular data with columns and expandable rows, making it useful for file browsers, database viewers, and structured lists.

Building a Complete Application

Now let's combine everything into a practical example. The following program is a simple unit converter that converts temperatures between Fahrenheit and Celsius. It uses ttk widgets, the grid geometry manager, and event handling.

import tkinter as tk
from tkinter import ttk


class TemperatureConverter:
    def __init__(self, root):
        self.root = root
        self.root.title("Temperature Converter")
        self.root.geometry("350x200")
        self.root.resizable(False, False)

        # Configure grid weights
        root.columnconfigure(0, weight=1)
        root.columnconfigure(1, weight=2)

        # Input field
        ttk.Label(root, text="Temperature:").grid(
            row=0, column=0, sticky=tk.W, padx=10, pady=10
        )
        self.temp_var = tk.StringVar()
        self.temp_entry = ttk.Entry(root, textvariable=self.temp_var, width=20)
        self.temp_entry.grid(row=0, column=1, sticky=tk.EW, padx=10, pady=10)

        # Direction selector
        ttk.Label(root, text="Convert:").grid(
            row=1, column=0, sticky=tk.W, padx=10, pady=5
        )
        self.direction = tk.StringVar(value="f_to_c")
        ttk.Radiobutton(
            root, text="Fahrenheit to Celsius",
            variable=self.direction, value="f_to_c"
        ).grid(row=1, column=1, sticky=tk.W, padx=10)
        ttk.Radiobutton(
            root, text="Celsius to Fahrenheit",
            variable=self.direction, value="c_to_f"
        ).grid(row=2, column=1, sticky=tk.W, padx=10)

        # Convert button
        ttk.Button(root, text="Convert", command=self.convert).grid(
            row=3, column=0, columnspan=2, pady=15
        )

        # Result label
        self.result_var = tk.StringVar(value="Enter a temperature above")
        ttk.Label(root, textvariable=self.result_var, font=("Arial", 12)).grid(
            row=4, column=0, columnspan=2, pady=5
        )

        # Bind Enter key to convert
        self.temp_entry.bind("<Return>", lambda e: self.convert())
        self.temp_entry.focus()

    def convert(self):
        try:
            temp = float(self.temp_var.get())
            if self.direction.get() == "f_to_c":
                result = (temp - 32) * 5 / 9
                self.result_var.set(f"{temp:.1f} F = {result:.1f} C")
            else:
                result = temp * 9 / 5 + 32
                self.result_var.set(f"{temp:.1f} C = {result:.1f} F")
        except ValueError:
            self.result_var.set("Please enter a valid number")


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

This example demonstrates several important patterns. The application logic is organized inside a class, which keeps the code clean and makes it easier to manage state. The StringVar objects provide two-way data binding between the widgets and the application logic. The bind() method on the entry field allows the user to press Enter instead of clicking the button, which is a small touch that improves usability significantly.

Notice how the grid geometry manager keeps everything aligned. Labels sit in column 0, inputs in column 1, and the button and result span both columns using columnspan=2. The sticky parameter ensures that labels align to the left (tk.W) and the entry field stretches to fill its cell (tk.EW).

Key Takeaways

  1. Tkinter ships with Python. There is nothing to install on Windows or macOS. Linux users may need the python3-tk package. Run python -m tkinter to verify your installation.
  2. Every application needs a root window and a main loop. Create a tk.Tk() instance, add your widgets, and call mainloop() to start processing events.
  3. Use ttk widgets whenever possible. The themed widget set in tkinter.ttk provides native-looking controls and includes exclusive widgets like Combobox, Notebook, Progressbar, and Treeview.
  4. Grid is the recommended geometry manager. It handles complex layouts more cleanly than pack, aligns widgets in both dimensions, and is easier to reason about. Use pack for simple vertical or horizontal stacking, and avoid place unless you need absolute positioning.
  5. Never block the event loop. Use after() for scheduled tasks and the threading module for long-running operations. Calling time.sleep() inside a Tkinter app will freeze the interface.

Tkinter may not be the flashiest GUI toolkit available for Python, but it is the one that requires the least setup and produces results the fastest. For learning GUI programming concepts, for building internal tools, and for putting a front end on scripts that would otherwise live in the terminal, it is a reliable and well-supported choice. Start with a simple window, add a few widgets, wire up your events, and build from there.

back to articles