🐍 Python·Mar 2026·2 reference files · 20 min read

tkinter-patterns

A comprehensive reference for writing clean, professional, and responsive Python desktop GUIs with Tkinter and ttk. Covers all three layout managers and when to use each, ttk theming and custom styles, building truly resizable windows with correct weight configuration, standard and custom dialogs, sidebar navigation, background threading without freezing the UI, sortable Treeview tables, and two complete production-ready app boilerplates.

packgridplacettk.Stylecolumnconfigureweightstickyttk.Treeviewttk.NotebookToplevelroot.after()threadingfiledialogmessageboxscrollable-canvastheme_use

Why Tkinter — and What This Skill Covers

Tkinter is Python's built-in GUI toolkit — no installation required, ships with every standard Python distribution, and produces native-looking applications on Windows, macOS, and Linux. For tools, utilities, dashboards, and internal applications, Tkinter combined with the ttk themed widget set produces genuinely professional results.

3
Layout
managers
0
Dependencies
to install
3
Full app
boilerplates

The most common source of Tkinter bugs is not the widgets themselves but two fundamental mistakes: mixing layout managers on the same parent container, and forgetting weight configuration on rows and columns that are supposed to resize. This skill addresses both issues directly in every layout section.

1 — Layout Managers

❌ The rule you must never break

Never mix pack and grid on the same parent widget. Tkinter will enter an infinite layout loop trying to satisfy both managers simultaneously, and your window will either freeze or produce completely wrong geometry. Use one manager per container. You can use different managers in different nested frames — just not in the same one.

ManagerBest ForAvoid When
packToolbars, button rows, simple top/bottom/left/right splitsAnything that needs column alignment
gridForms, settings panels, aligned rows/columns — the most powerfulSimple linear stacking (overkill)
placeOverlays, floating badges, drag handles, splash screensGeneral layout — never use for main structure

pack — Linear Stacking

pack stacks widgets along an axis. The most important options are fill (which axis to stretch along) and expand (claim leftover space). Forgetting expand=True when you want a frame to fill remaining space is the single most common pack bug.

pythonpack — two-pane layout
import tkinter as tk

root = tk.Tk()

header = tk.Label(root, text="Header", bg="steelblue", fg="white")
header.pack(side=tk.TOP, fill=tk.X, padx=4, pady=4)

sidebar = tk.Frame(root, width=160, bg="#f0f0f0")
sidebar.pack(side=tk.LEFT, fill=tk.Y)           # fills full height, fixed width

main = tk.Frame(root, bg="white")
main.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)  # ← expand=True is critical

root.mainloop()
OptionValuesEffect
sideTOP / BOTTOM / LEFT / RIGHTWhich edge to stack against
fillX / Y / BOTH / NONEStretch to fill allocated space
expandTrue / FalseClaim leftover space in the window
padx / padyint or (left, right)Outer padding
ipadx / ipadyintInner padding
anchortk.W / CENTER / ...Position inside allocated cell

grid — Table Layout (Most Powerful)

grid places widgets in a row/column table. It is the right choice for any layout with aligned columns — forms, dashboards, settings panels. The critical rule: always call columnconfigure(col, weight=1)and rowconfigure(row, weight=1) for any row or column that should grow when the window is resized.

pythongrid — resizable form
root = tk.Tk()
root.columnconfigure(1, weight=1)   # column 1 (entry column) grows horizontally

labels = ["Name", "Email", "Phone"]

for row, label in enumerate(labels):
    tk.Label(root, text=label, anchor="w").grid(
        row=row, column=0, sticky="w", padx=(8, 4), pady=4
    )
    e = tk.Entry(root)
    e.grid(row=row, column=1, sticky="ew", padx=(4, 8), pady=4)
    #                                ↑↑↑
    # sticky="ew" makes the entry stretch left and right as the window resizes

root.mainloop()
OptionEffect
sticky="nsew"Stretch widget to fill cell (compass: n/s/e/w)
columnspan=NMerge N columns
rowspan=NMerge N rows
padx / padyOuter padding — accepts (left, right) tuple

place — Absolute & Relative Positioning

place positions widgets at exact pixel or fractional coordinates. Useful for overlays and decorative elements, but produces non-resizable layouts when used for main structure. Use it for floating badges, corner labels, and splash screen elements only.

pythonplace — overlay badge
frame = tk.Frame(root, bg="white")
frame.place(relx=0, rely=0, relwidth=1, relheight=1)   # fills entire parent

badge = tk.Label(frame, text="NEW", bg="red", fg="white", font=("Segoe UI", 8, "bold"))
badge.place(relx=1.0, rely=0.0, anchor="ne", x=-4, y=4)  # top-right corner, 4px inset

2 — ttk Theming & Styling

Always prefer tkinter.ttk widgets over classic tk.* widgets. The ttk widget set respects the OS theme on each platform and supports fully custom styling through ttk.Style. A tk.Button cannot be styled the same way — use ttk.Button in all new code.

pythonttk setup + built-in themes
import tkinter as tk
from tkinter import ttk

root = tk.Tk()
style = ttk.Style()
print(style.theme_names())   # ('clam', 'alt', 'default', 'classic', ...)
style.theme_use("clam")      # best cross-platform starting point
ThemeLookAvailable On
clamModern flat — best cross-platform defaultAll platforms
altSlightly raised buttonsAll platforms
defaultBasic systemAll platforms
vista / winnativeWindows native AeroWindows only
aquamacOS nativemacOS only

Styling Widget Classes

pythonttk.Style — configure and map
style = ttk.Style()
style.theme_use("clam")

# style.configure("WidgetClass", **options)
style.configure("TButton",
    font=("Segoe UI", 10),
    padding=(10, 6),
    background="#2563EB",
    foreground="white",
)
# style.map defines state-dependent styles
style.map("TButton",
    background=[("active", "#1D4ED8"), ("disabled", "#94A3B8")],
    foreground=[("disabled", "#CBD5E1")],
)

style.configure("TLabel",    font=("Segoe UI", 10))
style.configure("TEntry",    fieldbackground="#F8FAFC", padding=6)
style.configure("Heading.TLabel", font=("Segoe UI", 14, "bold"))

Custom Named Styles (Variants)

Prefix any style name with a custom word followed by a dot to create a named variant that can be applied to individual widgets. The suffix must match a real widget class name (e.g. .TButton, .TLabel).

pythonnamed style variants
style.configure("Primary.TButton", background="#2563EB", foreground="white")
style.configure("Danger.TButton",  background="#DC2626", foreground="white")
style.configure("Ghost.TButton",   background="transparent", relief="flat")

# Apply to a specific widget:
btn_delete = ttk.Button(root, text="Delete", style="Danger.TButton")
btn_save   = ttk.Button(root, text="Save",   style="Primary.TButton")

ttk Widget Quick Reference

pythoncommon ttk widgets
# Entry with StringVar
var = tk.StringVar()
entry = ttk.Entry(frame, textvariable=var, width=30)

# Combobox (dropdown)
combo = ttk.Combobox(frame, values=["Option A", "Option B"], state="readonly")

# Checkbutton with BooleanVar
checked = tk.BooleanVar()
chk = ttk.Checkbutton(frame, text="Enable feature", variable=checked)

# Progressbar
pb = ttk.Progressbar(frame, mode="determinate", maximum=100)
pb["value"] = 65    # set current value

# Horizontal separator
ttk.Separator(frame, orient="horizontal").pack(fill=tk.X, pady=8)

# Notebook (tabbed interface)
nb = ttk.Notebook(frame)
tab1 = ttk.Frame(nb)
tab2 = ttk.Frame(nb)
nb.add(tab1, text="  Overview  ")
nb.add(tab2, text="  Details   ")
nb.pack(fill=tk.BOTH, expand=True)

# Treeview (table or tree)
tree = ttk.Treeview(frame, columns=("Name", "Value"), show="headings")
tree.heading("Name",  text="Name")
tree.heading("Value", text="Value")
tree.column("Name",  width=200)
tree.column("Value", width=100, anchor="e")
tree.insert("", tk.END, values=("CPU Usage", "42%"))

3 — Responsive & Resizable Windows

A Tkinter window that doesn't resize correctly is almost always missing weight configuration on its rows and columns. Weight tells the geometry manager how to distribute leftover space when the window grows. A weight of 0 means fixed size. A weight of 1 means "take a proportional share of available space."

✓ The golden rule for resizable layouts

Every container that should grow must have weight > 0 set on the rows/columns that contain expanding children. This applies all the way up the widget tree — if the root window doesn't have columnconfigure(0, weight=1), nothing inside it can expand.

Window Setup Sequence

pythonroot window — always configure this
root = tk.Tk()
root.title("My App")
root.geometry("900x600")          # initial size
root.minsize(600, 400)            # prevent collapsing to nothing
root.columnconfigure(0, weight=1) # root's single column expands
root.rowconfigure(0, weight=1)    # root's main row expands

Three-Column Layout with Fixed Sidebars

pythonsidebar | main | panel layout
root.columnconfigure(0, weight=0)   # sidebar: fixed width
root.columnconfigure(1, weight=1)   # main area: takes all leftover space
root.columnconfigure(2, weight=0)   # right panel: fixed width
root.rowconfigure(0, weight=1)      # single row fills height

sidebar = ttk.Frame(root, width=200)
sidebar.grid(row=0, column=0, sticky="ns")
sidebar.grid_propagate(False)   # ← prevents children from shrinking the frame

main = ttk.Frame(root)
main.grid(row=0, column=1, sticky="nsew")   # fills all remaining space

panel = ttk.Frame(root, width=240)
panel.grid(row=0, column=2, sticky="ns")
panel.grid_propagate(False)

Scrollable Content Area

Tkinter has no built-in scrollable frame. The standard workaround uses a Canvas with a Scrollbar, placing an inner Frame inside the canvas as a window item. All scrollable content goes inside the inner frame.

pythonmake_scrollable() — reusable helper
def make_scrollable(parent):
    """Return an inner Frame. Pack all scrollable content into it."""
    canvas = tk.Canvas(parent, highlightthickness=0)
    vsb    = ttk.Scrollbar(parent, orient="vertical", command=canvas.yview)
    canvas.configure(yscrollcommand=vsb.set)

    vsb.pack(side=tk.RIGHT, fill=tk.Y)
    canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

    inner     = ttk.Frame(canvas)
    window_id = canvas.create_window((0, 0), window=inner, anchor="nw")

    def on_inner_resize(event):
        canvas.configure(scrollregion=canvas.bbox("all"))

    def on_canvas_resize(event):
        canvas.itemconfig(window_id, width=event.width)   # inner matches canvas width

    inner.bind("<Configure>",  on_inner_resize)
    canvas.bind("<Configure>", on_canvas_resize)

    # Mouse wheel scrolling (cross-platform)
    canvas.bind_all(
        "<MouseWheel>",
        lambda e: canvas.yview_scroll(-1 * (e.delta // 120), "units")
    )

    return inner   # ← put your widgets here

# Usage:
scrollable = make_scrollable(some_frame)
for i in range(50):
    ttk.Label(scrollable, text=f"Row {i}").pack(anchor="w", pady=2)

Binding to Window Resize

pythondynamic resize callback
def on_resize(event):
    w, h = event.width, event.height
    # Recalculate anything that depends on window size
    canvas.itemconfig(bg_rect, width=w, height=h)

root.bind("<Configure>", on_resize)

4 — Forms, Dialogs & Menus

Form with Validation

pythonLoginForm — class-based with validation
import tkinter as tk
from tkinter import ttk, messagebox


class LoginForm(ttk.Frame):
    def __init__(self, parent):
        super().__init__(parent, padding=24)
        self.grid(row=0, column=0, sticky="nsew")
        parent.columnconfigure(0, weight=1)
        parent.rowconfigure(0, weight=1)
        self._build()

    def _build(self):
        ttk.Label(self, text="Sign In", font=("Segoe UI", 16, "bold")).grid(
            row=0, column=0, columnspan=2, pady=(0, 16), sticky="w"
        )
        fields = [("Email", "email"), ("Password", "password")]
        self.vars = {}

        for i, (label, key) in enumerate(fields, start=1):
            ttk.Label(self, text=label).grid(row=i, column=0, sticky="w", pady=4)
            var  = tk.StringVar()
            self.vars[key] = var
            show = "*" if key == "password" else ""
            ttk.Entry(self, textvariable=var, show=show, width=32).grid(
                row=i, column=1, sticky="ew", padx=(8, 0), pady=4
            )

        self.columnconfigure(1, weight=1)
        ttk.Button(self, text="Sign In", style="Primary.TButton",
                   command=self._submit).grid(
            row=10, column=0, columnspan=2, sticky="ew", pady=(16, 0)
        )

    def _submit(self):
        email    = self.vars["email"].get().strip()
        password = self.vars["password"].get()
        if not email or "@" not in email:
            messagebox.showwarning("Validation", "Enter a valid email address.")
            return
        if len(password) < 6:
            messagebox.showwarning("Validation", "Password must be at least 6 characters.")
            return
        messagebox.showinfo("Success", f"Logged in as {email}")

Standard Built-in Dialogs

pythonmessagebox, filedialog, simpledialog
from tkinter import messagebox, filedialog, simpledialog, colorchooser

# Message boxes
messagebox.showinfo("Title",    "Information message")
messagebox.showwarning("Title", "Warning message")
messagebox.showerror("Title",   "Error message")
ok  = messagebox.askokcancel("Confirm", "Proceed?")     # → True / False
yes = messagebox.askyesno("Confirm",    "Are you sure?") # → True / False

# File pickers
path      = filedialog.askopenfilename(
    title="Open File",
    filetypes=[("Python files", "*.py"), ("All files", "*.*")]
)
save_path = filedialog.asksaveasfilename(defaultextension=".txt")
folder    = filedialog.askdirectory(title="Select Folder")

# Simple text / number input
name = simpledialog.askstring("Input", "Enter your name:")
num  = simpledialog.askinteger("Input", "Enter a number:", minvalue=1, maxvalue=100)

# Color picker → ((r, g, b), '#rrggbb') or (None, None)
color = colorchooser.askcolor(title="Pick a colour")

Custom Modal Dialog

pythonConfirmDialog — blocking modal
class ConfirmDialog(tk.Toplevel):
    def __init__(self, parent, title, message):
        super().__init__(parent)
        self.title(title)
        self.resizable(False, False)
        self.result = False

        self.transient(parent)   # attach to parent window
        self.grab_set()          # block input to all other windows

        ttk.Label(self, text=message, wraplength=300, padding=16).pack()

        btn_frame = ttk.Frame(self, padding=(16, 0, 16, 16))
        btn_frame.pack(fill=tk.X)
        ttk.Button(btn_frame, text="Cancel",  command=self.destroy).pack(side=tk.RIGHT, padx=(4, 0))
        ttk.Button(btn_frame, text="Confirm", command=self._confirm).pack(side=tk.RIGHT)

        self.wait_window()   # blocks until the dialog is destroyed

    def _confirm(self):
        self.result = True
        self.destroy()

# Usage:
dlg = ConfirmDialog(root, "Delete Item", "This action cannot be undone. Continue?")
if dlg.result:
    delete_item()

Menu Bar & Context Menu

pythonmenu bar + keyboard shortcuts
def build_menu(root):
    menubar = tk.Menu(root)
    root.config(menu=menubar)

    file_menu = tk.Menu(menubar, tearoff=False)
    menubar.add_cascade(label="File", menu=file_menu)
    file_menu.add_command(label="New",    accelerator="Ctrl+N", command=on_new)
    file_menu.add_command(label="Open…",  accelerator="Ctrl+O", command=on_open)
    file_menu.add_separator()
    file_menu.add_command(label="Exit",   command=root.quit)

    edit_menu = tk.Menu(menubar, tearoff=False)
    menubar.add_cascade(label="Edit", menu=edit_menu)
    edit_menu.add_command(label="Preferences", command=open_settings)

    # Keyboard shortcut bindings
    root.bind_all("<Control-n>", lambda e: on_new())
    root.bind_all("<Control-o>", lambda e: on_open())


def attach_context_menu(widget, items):
    """items: list of (label, command) or None for separator."""
    menu = tk.Menu(widget, tearoff=False)
    for item in items:
        if item is None:
            menu.add_separator()
        else:
            menu.add_command(label=item[0], command=item[1])

    def show(event):
        menu.tk_popup(event.x_root, event.y_root)

    widget.bind("<Button-3>", show)   # right-click on Linux/Windows
    widget.bind("<Button-2>", show)   # right-click on macOS

5 — Reusable Components

Status Bar with Progress

pythonStatusBar widget
class StatusBar(ttk.Frame):
    def __init__(self, parent):
        super().__init__(parent, relief="sunken")
        self.pack(side=tk.BOTTOM, fill=tk.X)

        self._var = tk.StringVar(value="Ready")
        ttk.Label(self, textvariable=self._var, anchor="w", padding=(6, 2)).pack(
            side=tk.LEFT, fill=tk.X, expand=True
        )
        self._progress = ttk.Progressbar(self, length=120, mode="determinate")
        self._progress.pack(side=tk.RIGHT, padx=6, pady=2)

    def set(self, message: str, progress: int | None = None):
        self._var.set(message)
        if progress is not None:
            self._progress["value"] = progress

    def clear(self):
        self._var.set("Ready")
        self._progress["value"] = 0

Toast Notification

pythonshow_toast() — temporary overlay
def show_toast(root, message, duration_ms=2500):
    toast = tk.Toplevel(root)
    toast.overrideredirect(True)          # no title bar or window chrome
    toast.attributes("-topmost", True)
    toast.attributes("-alpha", 0.92)

    tk.Label(
        toast, text=message,
        bg="#1E293B", fg="white",
        font=("Segoe UI", 10),
        padx=16, pady=10,
    ).pack()

    # Position: bottom-right corner of the root window
    root.update_idletasks()
    rx, ry = root.winfo_x(), root.winfo_y()
    rw, rh = root.winfo_width(), root.winfo_height()
    toast.update_idletasks()
    tw, th = toast.winfo_width(), toast.winfo_height()
    toast.geometry(f"+{rx + rw - tw - 16}+{ry + rh - th - 40}")

    root.after(duration_ms, toast.destroy)   # auto-dismiss

Sidebar Navigation

pythonSidebarNav — icon + label, active state
class SidebarNav(ttk.Frame):
    ITEMS = [
        ("🏠", "Dashboard"),
        ("📦", "Products"),
        ("⚙️", "Settings"),
    ]

    def __init__(self, parent, on_select):
        super().__init__(parent, style="Sidebar.TFrame", width=180)
        self.pack_propagate(False)   # prevent children from shrinking the sidebar
        self.on_select = on_select
        self._buttons  = {}
        self._active   = None
        self._build()

    def _build(self):
        for icon, label in self.ITEMS:
            btn = ttk.Button(
                self,
                text=f"  {icon}  {label}",
                style="Nav.TButton",
                command=lambda l=label: self._select(l),   # ← capture by value
            )
            btn.pack(fill=tk.X, padx=8, pady=2)
            self._buttons[label] = btn
        self._select(self.ITEMS[0][1])   # activate first item

    def _select(self, label):
        self._active = label
        self.on_select(label)

Sortable Treeview Table

pythonSortableTable — click header to sort
class SortableTable(ttk.Treeview):
    def __init__(self, parent, columns: list[str], **kwargs):
        super().__init__(parent, columns=columns, show="headings", **kwargs)
        self._sort_col = None
        self._sort_asc = True

        for col in columns:
            self.heading(col, text=col, command=lambda c=col: self._sort(c))
            self.column(col, minwidth=60, stretch=True)

    def _sort(self, col):
        if self._sort_col == col:
            self._sort_asc = not self._sort_asc   # toggle direction
        else:
            self._sort_col = col
            self._sort_asc = True

        rows = [(self.set(k, col), k) for k in self.get_children("")]
        rows.sort(reverse=not self._sort_asc)
        for i, (_, k) in enumerate(rows):
            self.move(k, "", i)

    def load(self, data: list[tuple]):
        self.delete(*self.get_children())
        for row in data:
            self.insert("", tk.END, values=row)

# Usage:
table = SortableTable(frame, columns=["Name", "Size", "Modified"])
table.pack(fill=tk.BOTH, expand=True)
table.load([("report.pdf", "1.2 MB", "2026-03-28"), ("data.csv", "450 KB", "2026-03-27")])

6 — Background Threading Without Freezing the UI

❌ The cause of every frozen Tkinter window

Calling time.sleep(), making a network request, or running any blocking operation directly in the main thread blocks Tkinter's event loop. The window stops responding to clicks, resize events, and repaints — it appears frozen and may go grey on some platforms.

The correct pattern: run the blocking work in a daemon thread. When the work is done, schedule a GUI update back on the main thread using root.after(0, callback). Never touch a widget directly from a background thread.

pythonbackground thread + safe GUI update
import threading

def run_in_background(root, task_fn, on_done):
    """
    Run task_fn() in a background thread.
    Call on_done(result) on the main thread when complete.
    """
    def worker():
        result = task_fn()
        root.after(0, lambda: on_done(result))   # ← safe: scheduled on main thread

    threading.Thread(target=worker, daemon=True).start()


# ── Example: fetch data without freezing the window ──
def fetch_data() -> str:
    import time
    time.sleep(2)           # simulate slow network request
    return "Fetched result"

def on_data_ready(result: str):
    label.config(text=result)
    btn.config(state=tk.NORMAL)

def start_fetch():
    btn.config(state=tk.DISABLED)
    label.config(text="Loading…")
    run_in_background(root, fetch_data, on_data_ready)

Periodic Updates with after()

For repeating tasks like a clock, live data refresh, or progress polling, use root.after(ms, callback) instead of a loop or time.sleep(). The callback is called on the main thread — safe to update any widget.

pythonClockLabel — self-rescheduling widget
class ClockLabel(ttk.Label):
    def __init__(self, parent, **kwargs):
        super().__init__(parent, **kwargs)
        self._tick()

    def _tick(self):
        from datetime import datetime
        self.configure(text=datetime.now().strftime("%H:%M:%S"))
        self.after(1000, self._tick)   # reschedule every 1 second

7 — Full App Boilerplates

The skill includes two production-ready boilerplates. Both use a class-based architecture that subclasses tk.Tk directly — cleaner than a standalone function, and easier to extend with methods.

BOILERPLATE 01
Minimal Class-based App
Header + body + status bar layout. Includes theme setup with ttk.Style, correct columnconfigure/ rowconfigure weight configuration, a settings hook, and set_status() for updating the status bar from anywhere.
BOILERPLATE 02
Multi-Page Notebook App
Uses ttk.Notebook for tab-based navigation. Each tab is an independent ttk.Frame with its own grid configuration. Good starting point for tools with clearly separated sections.
BOILERPLATE 03
Sidebar Navigation App (Frame Switcher)
Dark sidebar using ttk.Style with active state highlighting. Each page is a hidden ttk.Frame that is shown/hidden via grid_remove() and grid() — no frame destruction and recreation, preserving widget state between page switches.

Sidebar Navigation App — Full Code

pythonSidebarApp — production skeleton
import tkinter as tk
from tkinter import ttk

PAGES = {"Dashboard": "📊", "Files": "📁", "Settings": "⚙️"}


class SidebarApp(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Sidebar App")
        self.geometry("960x640")
        self.minsize(700, 480)
        self.columnconfigure(1, weight=1)   # content column expands
        self.rowconfigure(0, weight=1)

        style = ttk.Style(self)
        style.theme_use("clam")
        style.configure("Sidebar.TFrame",  background="#1E293B")
        style.configure("Nav.TLabel",
            background="#1E293B", foreground="#CBD5E1",
            font=("Segoe UI", 11), padding=(12, 8))
        style.configure("NavActive.TLabel",
            background="#2563EB", foreground="white",
            font=("Segoe UI", 11, "bold"), padding=(12, 8))

        self._frames: dict[str, ttk.Frame] = {}
        self._nav_labels: dict[str, ttk.Label] = {}
        self._active_page = None

        self._build_sidebar()
        self._build_content()
        self._show_page("Dashboard")

    def _build_sidebar(self):
        sidebar = ttk.Frame(self, style="Sidebar.TFrame", width=200)
        sidebar.grid(row=0, column=0, sticky="ns")
        sidebar.grid_propagate(False)

        for page, icon in PAGES.items():
            lbl = ttk.Label(sidebar, text=f" {icon}  {page}",
                            style="Nav.TLabel", cursor="hand2")
            lbl.pack(fill=tk.X)
            lbl.bind("<Button-1>", lambda e, p=page: self._show_page(p))
            self._nav_labels[page] = lbl

    def _build_content(self):
        for page in PAGES:
            frame = ttk.Frame(self, padding=24)
            frame.columnconfigure(0, weight=1)
            frame.rowconfigure(1, weight=1)
            ttk.Label(frame, text=page, font=("Segoe UI", 18, "bold")).grid(sticky="w")
            ttk.Label(frame, text=f"Content for {page} goes here.").grid(
                row=1, sticky="nw", pady=8)
            frame.grid(row=0, column=1, sticky="nsew")
            frame.grid_remove()   # hidden initially
            self._frames[page] = frame

    def _show_page(self, page: str):
        if self._active_page:
            self._frames[self._active_page].grid_remove()
            self._nav_labels[self._active_page].configure(style="Nav.TLabel")
        self._frames[page].grid()
        self._nav_labels[page].configure(style="NavActive.TLabel")
        self._active_page = page


if __name__ == "__main__":
    SidebarApp().mainloop()

Light / Dark Theme Toggle

pythonapply_theme() — runtime switching
THEMES = {
    "light": {"bg": "#F8FAFC", "fg": "#0F172A", "entry_bg": "#FFFFFF",  "accent": "#2563EB"},
    "dark":  {"bg": "#0F172A", "fg": "#F1F5F9", "entry_bg": "#1E293B",  "accent": "#3B82F6"},
}

def apply_theme(style: ttk.Style, root: tk.Tk, name: str):
    t = THEMES[name]
    style.configure("TFrame",  background=t["bg"])
    style.configure("TLabel",  background=t["bg"], foreground=t["fg"])
    style.configure("TEntry",  fieldbackground=t["entry_bg"], foreground=t["fg"])
    style.configure("TButton", background=t["accent"], foreground="white")
    root.configure(background=t["bg"])

Anti-Patterns to Avoid

❌ Don't✅ Do instead
Mix pack and grid on the same parentUse one manager per container — different frames can use different managers
Use place for the main layoutUse grid or pack; reserve place for overlays
Use tk.Button with a ttk themeUse ttk.Button — classic widgets ignore ttk.Style
Hardcode pixel sizes for all widths/heightsUse weight + sticky to let the layout breathe
Call time.sleep() in the main threadUse root.after(ms, callback) for delays
Run blocking I/O on the main threadUse threading.Thread(daemon=True) + root.after(0, cb)
Touch widgets from a background threadSchedule updates with root.after(0, lambda: widget.config(...))
Build layout in global scopeWrap everything in a class or main() function
Forget expand=True with packpack(fill=tk.BOTH, expand=True) for growing frames
Forget columnconfigure(weight=1)Configure weights on every container in the hierarchy that should resize

Pre-Release Checklist

  • No pack and grid mixed on the same parent widget
  • Root window has columnconfigure(0, weight=1) and rowconfigure(N, weight=1)
  • Every expanding frame has weight configuration on the row/column containing growing children
  • All blocking operations (network, file I/O, sleep) run in a daemon thread
  • All GUI updates from threads scheduled via root.after(0, callback)
  • All ttk.* widgets used instead of tk.* equivalents
  • root.minsize() set to prevent the window from collapsing
  • All custom dialogs use transient(parent) + grab_set()
  • Tested by resizing the window to verify all frames expand correctly
AI Skill File

Download tkinter-patterns Skill

This .skill file contains the full SKILL.md guide plus two reference files — ready to load into Claude or any AI tool as expert context for all your Tkinter and ttk questions.

3 rule files
All 3 layout managers
ttk styling guide
3 full boilerplates
Threading patterns
Sortable Treeview
⬇ Download Skill File

Hosted by ZynU Host · host.zynu.net