Free Ebook cover Desktop Apps with Tkinter: A Beginner’s Guide to Python GUIs

Desktop Apps with Tkinter: A Beginner’s Guide to Python GUIs

New course

10 pages

Designing Tkinter Windows and Application Structure

Capítulo 2

Estimated reading time: 9 minutes

+ Exercise

Why structure matters in Tkinter

Tkinter lets you build a working interface quickly, but a “script-style” file (widgets created at top level, callbacks scattered around) becomes hard to extend: you lose track of widget references, configuration is mixed with layout, and event handlers depend on global variables. A simple, consistent structure solves this by giving your app a single owner object that holds the window, widgets, and behavior.

A practical pattern for beginners is: create an application class that owns the root window, builds widgets in dedicated methods, and implements callbacks as instance methods. This keeps state in one place and makes refactoring and testing easier.

A simple application class pattern

The goal is to build an App class with a clear internal organization:

  • Configuration methods: window title, size, minsize, theme options.
  • Widget creation: instantiate widgets and store them on self.
  • Layout methods: place widgets using grid/pack in one place.
  • Logic methods: event handlers (callbacks) and other app behavior.

Start with a skeleton that makes these boundaries obvious.

import tkinter as tk
from tkinter import ttk

class App:
    def __init__(self, root: tk.Tk):
        self.root = root

        # Constants (sizes, padding)
        self.PAD = 10
        self.ENTRY_WIDTH = 30

        self._configure_window()
        self._create_widgets()
        self._layout_widgets()

    # --- configuration ---
    def _configure_window(self):
        self.root.title("Class-Based Tkinter App")
        self.root.minsize(420, 200)

    # --- widgets ---
    def _create_widgets(self):
        self.main = ttk.Frame(self.root, padding=self.PAD)

        self.title_label = ttk.Label(self.main, text="Name Formatter")

        self.name_var = tk.StringVar()
        self.name_entry = ttk.Entry(self.main, textvariable=self.name_var, width=self.ENTRY_WIDTH)

        self.upper_btn = ttk.Button(self.main, text="UPPERCASE", command=self.on_uppercase)
        self.lower_btn = ttk.Button(self.main, text="lowercase", command=self.on_lowercase)
        self.clear_btn = ttk.Button(self.main, text="Clear", command=self.on_clear)

        self.result_var = tk.StringVar(value="")
        self.result_label = ttk.Label(self.main, textvariable=self.result_var)

    # --- layout ---
    def _layout_widgets(self):
        self.main.grid(row=0, column=0, sticky="nsew")

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

        self.main.columnconfigure(0, weight=1)
        self.main.columnconfigure(1, weight=1)
        self.main.columnconfigure(2, weight=1)

        self.title_label.grid(row=0, column=0, columnspan=3, sticky="w", pady=(0, self.PAD))

        self.name_entry.grid(row=1, column=0, columnspan=3, sticky="ew", pady=(0, self.PAD))

        self.upper_btn.grid(row=2, column=0, sticky="ew")
        self.lower_btn.grid(row=2, column=1, sticky="ew", padx=(self.PAD, self.PAD))
        self.clear_btn.grid(row=2, column=2, sticky="ew")

        self.result_label.grid(row=3, column=0, columnspan=3, sticky="w", pady=(self.PAD, 0))

        self.name_entry.focus()

    # --- logic (callbacks) ---
    def on_uppercase(self):
        text = self.name_var.get().strip()
        self.result_var.set(text.upper())

    def on_lowercase(self):
        text = self.name_var.get().strip()
        self.result_var.set(text.lower())

    def on_clear(self):
        self.name_var.set("")
        self.result_var.set("")
        self.name_entry.focus()


def main():
    root = tk.Tk()
    app = App(root)
    root.mainloop()

if __name__ == "__main__":
    main()

What this structure gives you

  • One place for state: widget references like self.name_entry and variables like self.name_var are easy to access inside methods.
  • Readable flow: __init__ reads like a checklist: configure → create → layout.
  • Safer callbacks: callbacks are methods, so they naturally use self instead of globals.

Separating concerns: configuration, layout, logic

1) Configuration methods: keep window setup together

Put window-level settings in a dedicated method so you can find and adjust them quickly. Typical items include title, minimum size, resizing behavior, and top-level grid/pack configuration.

Continue in our app.

You can listen to the audiobook with the screen off, receive a free certificate for this course, and also have access to 5,000 other free online courses.

Or continue reading below...
Download App

Download the app

def _configure_window(self):
    self.root.title("My App")
    self.root.minsize(500, 300)

    # If you use grid at the root level, configure weights here
    self.root.columnconfigure(0, weight=1)
    self.root.rowconfigure(0, weight=1)

If you later add menus, keyboard shortcuts, or a status bar, you’ll know where “window concerns” live.

2) Layout methods: place widgets in one place

A common source of confusion is mixing widget creation and layout. Instead of doing ttk.Button(...).grid(...) inline, store the widget first, then lay it out in a separate method. This makes it easier to change layouts without touching widget configuration.

def _create_widgets(self):
    self.save_btn = ttk.Button(self.main, text="Save", command=self.on_save)

def _layout_widgets(self):
    self.save_btn.grid(row=0, column=0, sticky="ew")

This separation also helps when you need to temporarily hide/show widgets or switch layouts based on app state.

3) Logic methods: event handlers and application behavior

Callbacks should be short and focused. A good rule: callbacks gather input from widgets, call a helper method if needed, and then update the UI.

def on_save(self):
    data = self._collect_form_data()
    ok, message = self._validate(data)
    if not ok:
        self.status_var.set(message)
        return
    self._write_to_disk(data)
    self.status_var.set("Saved")

Even in small apps, extracting helpers like _collect_form_data and _validate keeps callbacks readable.

Using constants for sizes, padding, and repeated values

Hard-coded numbers scattered across grid calls are difficult to maintain. Use constants stored on the class (or module-level constants) for repeated values such as padding, widths, and common spacing.

class App:
    PAD = 10
    SMALL_PAD = 6
    ENTRY_WIDTH = 34

    def __init__(self, root):
        self.root = root
        self._configure_window()
        self._create_widgets()
        self._layout_widgets()

    def _create_widgets(self):
        self.main = ttk.Frame(self.root, padding=self.PAD)
        self.search_entry = ttk.Entry(self.main, width=self.ENTRY_WIDTH)

    def _layout_widgets(self):
        self.search_entry.grid(row=0, column=0, padx=(0, self.SMALL_PAD))

Choose one approach and stick to it. Class attributes work well when the values are truly “design constants” for the whole app.

Naming widgets clearly (and consistently)

Clear naming reduces mental load when your UI grows. Prefer names that describe purpose, not just type.

  • Good: self.search_entry, self.status_label, self.save_btn, self.items_tree
  • Avoid: self.entry1, self.label2, self.button_ok (unless “ok” is a meaningful role)

Also name your variables by meaning: self.query_var is clearer than self.text_var if it holds a search query.

Step-by-step: refactor a script-style Tkinter file into a class-based app

This activity takes a typical small script and converts it into the structured pattern. The goal is not to add features, only to reorganize code.

Starting point: script-style code

In this style, widgets and callbacks are defined at module level and depend on global variables.

import tkinter as tk
from tkinter import ttk

root = tk.Tk()
root.title("Greeter")

name_var = tk.StringVar()

frame = ttk.Frame(root, padding=10)
frame.grid(row=0, column=0, sticky="nsew")

entry = ttk.Entry(frame, textvariable=name_var, width=30)
entry.grid(row=0, column=0, sticky="ew")

out = ttk.Label(frame, text="")
out.grid(row=1, column=0, sticky="w", pady=(10, 0))

def greet():
    name = name_var.get().strip()
    if not name:
        out.config(text="Please enter a name")
    else:
        out.config(text=f"Hello, {name}!")

btn = ttk.Button(frame, text="Greet", command=greet)
btn.grid(row=0, column=1, padx=(10, 0))

entry.focus()
root.mainloop()

Refactoring plan (follow in order)

  • Step 1: Create an App class that accepts root and stores it as self.root.
  • Step 2: Move window setup into _configure_window.
  • Step 3: Move widget creation into _create_widgets. Convert globals (name_var, entry, out) into instance attributes (self.name_var, self.name_entry, self.output_label).
  • Step 4: Move all grid calls into _layout_widgets.
  • Step 5: Turn greet() into on_greet(self) and update the button command to self.on_greet.
  • Step 6: Add constants for padding/width and replace magic numbers.

Result: class-based version

import tkinter as tk
from tkinter import ttk

class App:
    PAD = 10
    ENTRY_WIDTH = 30

    def __init__(self, root: tk.Tk):
        self.root = root
        self._configure_window()
        self._create_widgets()
        self._layout_widgets()

    def _configure_window(self):
        self.root.title("Greeter")
        self.root.columnconfigure(0, weight=1)
        self.root.rowconfigure(0, weight=1)

    def _create_widgets(self):
        self.name_var = tk.StringVar()

        self.main = ttk.Frame(self.root, padding=self.PAD)

        self.name_entry = ttk.Entry(self.main, textvariable=self.name_var, width=self.ENTRY_WIDTH)
        self.greet_btn = ttk.Button(self.main, text="Greet", command=self.on_greet)

        self.output_var = tk.StringVar(value="")
        self.output_label = ttk.Label(self.main, textvariable=self.output_var)

    def _layout_widgets(self):
        self.main.grid(row=0, column=0, sticky="nsew")
        self.main.columnconfigure(0, weight=1)

        self.name_entry.grid(row=0, column=0, sticky="ew")
        self.greet_btn.grid(row=0, column=1, padx=(self.PAD, 0))
        self.output_label.grid(row=1, column=0, columnspan=2, sticky="w", pady=(self.PAD, 0))

        self.name_entry.focus()

    def on_greet(self):
        name = self.name_var.get().strip()
        if not name:
            self.output_var.set("Please enter a name")
        else:
            self.output_var.set(f"Hello, {name}!")


def main():
    root = tk.Tk()
    app = App(root)
    root.mainloop()

if __name__ == "__main__":
    main()

Common structural tips for growing apps

Group related widgets into frames

As soon as you have more than a few widgets, create frames for sections (toolbar, form area, results area). Each frame can have its own layout rules, reducing grid complexity.

def _create_widgets(self):
    self.toolbar = ttk.Frame(self.root, padding=self.PAD)
    self.content = ttk.Frame(self.root, padding=self.PAD)

    self.refresh_btn = ttk.Button(self.toolbar, text="Refresh", command=self.on_refresh)
    self.listbox = tk.Listbox(self.content)

def _layout_widgets(self):
    self.toolbar.grid(row=0, column=0, sticky="ew")
    self.content.grid(row=1, column=0, sticky="nsew")

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

    self.refresh_btn.grid(row=0, column=0, sticky="w")
    self.listbox.grid(row=0, column=0, sticky="nsew")
    self.content.rowconfigure(0, weight=1)
    self.content.columnconfigure(0, weight=1)

Keep UI updates in one place when possible

If multiple callbacks update the same widgets, consider a small helper like _set_status or _render to centralize UI changes.

def _set_status(self, message: str):
    self.status_var.set(message)

def on_refresh(self):
    self._set_status("Refreshing...")
    # refresh work here
    self._set_status("Ready")

Prefer instance attributes over globals

When you store widgets and variables on self, you avoid global state and make it clear which parts belong to the app. If you find yourself using global in a Tkinter program, it’s usually a sign you should move that state into a class.

Now answer the exercise about the content:

In a class-based Tkinter app, what is the main benefit of storing widgets and Tkinter variables as instance attributes (for example, self.name_entry and self.name_var) and implementing callbacks as instance methods?

You are right! Congratulations, now go to the next page

You missed! Try again.

Keeping widgets and variables on self gives the app one clear place for state. Callbacks as instance methods can access that state directly without globals, improving readability and maintainability.

Next chapter

Tkinter Layouts: pack(), grid(), and place() for Desktop Apps

Arrow Right Icon
Download the app to earn free Certification and listen to the courses in the background, even with the screen off.