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

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

Capítulo 3

Estimated reading time: 11 minutes

+ Exercise

Why layout managers matter

Tkinter widgets do not automatically arrange themselves. A layout manager decides where each widget goes and how it behaves when the window resizes. Tkinter provides three geometry managers:

  • pack(): best for simple stacks (top/bottom/left/right), toolbars, sidebars.
  • grid(): best for rows/columns, forms, dialogs, dashboards.
  • place(): absolute or relative positioning; useful for very specific cases, but often avoided for resizable apps.

Important rule: do not mix pack and grid in the same container (the same parent widget). You can use pack in one Frame and grid in another.

1) pack(): simple vertical and horizontal stacks

pack() places widgets against a side of the container and lets them “stack” as you add more. It is ideal when your UI can be described as sections: a top bar, a left panel, a main area, a bottom status bar.

Key pack() options

  • side: top, bottom, left, right.
  • fill: x, y, or both to stretch in that direction.
  • expand: if True, widget gets extra space when the container grows.
  • padx, pady: outer padding (space outside the widget).
  • ipadx, ipady: inner padding (space inside the widget).
  • anchor: where the widget sits within its allocated space (e.g., w, e, center).

Practical goal: a header + content + status bar

This layout is common in desktop apps. Use pack() to stack sections vertically, then let the content area expand.

import tkinter as tkfrom tkinter import ttkroot = tk.Tk()root.title("pack() layout demo")root.geometry("520x320")header = ttk.Frame(root, padding=10)header.pack(side="top", fill="x")ttk.Label(header, text="Customer Manager", font=("Segoe UI", 14, "bold")).pack(side="left")ttk.Button(header, text="New").pack(side="right")content = ttk.Frame(root, padding=10)content.pack(side="top", fill="both", expand=True)ttk.Label(content, text="Main content area").pack(anchor="w")ttk.Text(content, height=8).pack(fill="both", expand=True, pady=(8, 0))status = ttk.Frame(root, padding=(10, 6))status.pack(side="bottom", fill="x")ttk.Label(status, text="Ready").pack(side="left")root.mainloop()

Step-by-step what happens:

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

  • The header is packed at the top and fills horizontally.
  • The status bar is packed at the bottom and fills horizontally.
  • The content frame is packed in the remaining space and expands both ways as the window grows.

Practical goal: a left sidebar + main panel

Use side="left" for a sidebar and let the main panel expand.

import tkinter as tkfrom tkinter import ttkroot = tk.Tk()root.title("Sidebar layout")root.geometry("640x360")sidebar = ttk.Frame(root, padding=10)sidebar.pack(side="left", fill="y")ttk.Label(sidebar, text="Menu", font=("Segoe UI", 11, "bold")).pack(anchor="w")for item in ("Dashboard", "Customers", "Invoices", "Settings"):    ttk.Button(sidebar, text=item).pack(fill="x", pady=4)main = ttk.Frame(root, padding=10)main.pack(side="left", fill="both", expand=True)ttk.Label(main, text="Main panel").pack(anchor="w")ttk.Entry(main).pack(fill="x", pady=(8, 0))root.mainloop()

When you need precise alignment in rows/columns (labels aligned with entries), switch to grid().

2) grid(): form-like layouts with rows and columns

grid() arranges widgets in a table. You choose a row and column for each widget, and you can span multiple rows/columns. This is the most common choice for dialogs and data-entry forms.

Key grid() options

  • row, column: cell position (0-based).
  • columnspan, rowspan: span across multiple cells.
  • padx, pady: outer padding around the widget.
  • sticky: how the widget sticks inside its cell: n, s, e, w (combine like "ew" or "nsew").

Two additional concepts control resizing behavior:

  • Widget expansion inside a cell: controlled by sticky. For example, sticky="ew" makes an entry stretch left-to-right within its cell.
  • How extra window space is distributed across the grid: controlled by rowconfigure and columnconfigure weights on the container.

Practical goal: a basic form with aligned labels and fields

import tkinter as tkfrom tkinter import ttkroot = tk.Tk()root.title("grid() form demo")root.geometry("520x240")form = ttk.Frame(root, padding=12)form.pack(fill="both", expand=True)ttk.Label(form, text="First name").grid(row=0, column=0, sticky="w", padx=(0, 8), pady=6)ttk.Entry(form).grid(row=0, column=1, sticky="ew", pady=6)ttk.Label(form, text="Last name").grid(row=1, column=0, sticky="w", padx=(0, 8), pady=6)ttk.Entry(form).grid(row=1, column=1, sticky="ew", pady=6)ttk.Label(form, text="Email").grid(row=2, column=0, sticky="w", padx=(0, 8), pady=6)ttk.Entry(form).grid(row=2, column=1, sticky="ew", pady=6)# Make column 1 (the entry column) grow when the window growsform.columnconfigure(1, weight=1)root.mainloop()

Step-by-step:

  • Labels are placed in column 0 and aligned left with sticky="w".
  • Entries are placed in column 1 and stretch horizontally with sticky="ew".
  • form.columnconfigure(1, weight=1) ensures the entry column receives extra width when resizing.

Padding patterns that keep UIs clean

Instead of sprinkling random padding values everywhere, use consistent spacing:

  • Use a container Frame with padding for overall margins.
  • Use pady=6 (or similar) for row spacing.
  • Use padx=(0, 8) on labels to create a consistent gap between label and field.

Row/column weights and resizing behavior

Weights decide how extra space is distributed. A column with weight=2 grows twice as fast as a column with weight=1. Rows work the same way.

Common patterns:

  • Make one main column expand: set weight on the content column only.
  • Make a text area expand vertically: set weight on the row containing it, and use sticky="nsew".
# Example: a notes field that expands both directionsttk.Label(form, text="Notes").grid(row=3, column=0, sticky="nw", padx=(0, 8), pady=6)notes = tk.Text(form, height=4)notes.grid(row=3, column=1, sticky="nsew", pady=6)form.columnconfigure(1, weight=1)form.rowconfigure(3, weight=1)

Without the row weight and sticky="nsew", the text widget will not grow vertically when the window grows.

3) place(): fixed positioning (and when to avoid it)

place() positions widgets using exact coordinates or relative coordinates. It can be useful for:

  • Small overlays (e.g., a badge on top of an image).
  • Custom canvas-like compositions when you truly want fixed geometry.

It is often avoided for typical desktop app layouts because fixed coordinates do not adapt well to:

  • Window resizing
  • Different fonts and DPI scaling
  • Localization (longer text labels)

Minimal place() example

import tkinter as tkfrom tkinter import ttkroot = tk.Tk()root.geometry("360x180")root.title("place() demo")panel = ttk.Frame(root)panel.pack(fill="both", expand=True)ttk.Label(panel, text="Fixed position").place(x=20, y=20)ttk.Button(panel, text="OK").place(relx=1.0, rely=1.0, x=-20, y=-20, anchor="se")root.mainloop()

Notice how the button uses relative positioning (relx, rely) plus an offset to stay near the bottom-right corner. Even with this, complex resizable forms are usually easier with grid().

Combining Frames to keep layouts manageable

Frames let you break a window into regions, and each region can use the most appropriate geometry manager. A common approach:

  • Use pack() to arrange major regions (toolbar, content, status bar).
  • Inside the content region, use grid() for structured layouts (forms, tables of controls).

This avoids mixing pack and grid in the same container while keeping the code readable.

Pattern: pack outer regions, grid inner form

import tkinter as tkfrom tkinter import ttkroot = tk.Tk()root.title("Frames + pack + grid")root.geometry("640x360")toolbar = ttk.Frame(root, padding=8)toolbar.pack(side="top", fill="x")ttk.Button(toolbar, text="Save").pack(side="left")ttk.Button(toolbar, text="Cancel").pack(side="left", padx=(6, 0))content = ttk.Frame(root, padding=12)content.pack(side="top", fill="both", expand=True)form = ttk.LabelFrame(content, text="Profile", padding=12)form.pack(fill="x")ttk.Label(form, text="Username").grid(row=0, column=0, sticky="w", padx=(0, 8), pady=6)ttk.Entry(form).grid(row=0, column=1, sticky="ew", pady=6)ttk.Label(form, text="Role").grid(row=1, column=0, sticky="w", padx=(0, 8), pady=6)ttk.Combobox(form, values=["Admin", "Editor", "Viewer"]).grid(row=1, column=1, sticky="ew", pady=6)form.columnconfigure(1, weight=1)root.mainloop()

Mini-project: a responsive form using Frames + grid with proper weight configuration

Goal: build a form that resizes nicely. When the window grows, the input fields and the notes area should expand. Buttons should stay aligned at the bottom-right. The layout should remain readable and consistent.

Step 1: Create the main regions with frames

We will use pack() for the top-level regions: a header, a body, and a footer. Inside the body we will use grid() for the form.

Step 2: Build the form with grid()

We will use two columns: labels (fixed) and inputs (expanding). We will also add a multi-line Notes field that expands both horizontally and vertically.

Step 3: Configure weights and sticky

To make the UI responsive:

  • Set the input column weight to 1 so it grows.
  • Set the notes row weight to 1 so it grows vertically.
  • Use sticky="ew" for single-line inputs and sticky="nsew" for the notes text area.

Complete mini-project code

import tkinter as tkfrom tkinter import ttkdef submit():    # In a real app, validate and save data here    status_var.set("Saved (demo)")def clear():    for var in (first_var, last_var, email_var, phone_var, country_var):        var.set("")    notes.delete("1.0", "end")    status_var.set("Cleared")root = tk.Tk()root.title("Responsive Form (Frames + grid)")root.geometry("720x420")# Top headerheader = ttk.Frame(root, padding=(12, 10))header.pack(side="top", fill="x")ttk.Label(header, text="Contact Details", font=("Segoe UI", 14, "bold")).pack(side="left")# Main body (will expand)body = ttk.Frame(root, padding=12)body.pack(side="top", fill="both", expand=True)# Footer/statusfooter = ttk.Frame(root, padding=(12, 8))footer.pack(side="bottom", fill="x")status_var = tk.StringVar(value="Ready")ttk.Label(footer, textvariable=status_var).pack(side="left")# Form frame inside bodyform = ttk.LabelFrame(body, text="Person", padding=12)form.pack(fill="both", expand=True)# Variablesfirst_var = tk.StringVar()last_var = tk.StringVar()email_var = tk.StringVar()phone_var = tk.StringVar()country_var = tk.StringVar()# Row 0ttk.Label(form, text="First name").grid(row=0, column=0, sticky="w", padx=(0, 10), pady=(0, 8))ttk.Entry(form, textvariable=first_var).grid(row=0, column=1, sticky="ew", pady=(0, 8))# Row 1ttk.Label(form, text="Last name").grid(row=1, column=0, sticky="w", padx=(0, 10), pady=(0, 8))ttk.Entry(form, textvariable=last_var).grid(row=1, column=1, sticky="ew", pady=(0, 8))# Row 2ttk.Label(form, text="Email").grid(row=2, column=0, sticky="w", padx=(0, 10), pady=(0, 8))ttk.Entry(form, textvariable=email_var).grid(row=2, column=1, sticky="ew", pady=(0, 8))# Row 3ttk.Label(form, text="Phone").grid(row=3, column=0, sticky="w", padx=(0, 10), pady=(0, 8))ttk.Entry(form, textvariable=phone_var).grid(row=3, column=1, sticky="ew", pady=(0, 8))# Row 4ttk.Label(form, text="Country").grid(row=4, column=0, sticky="w", padx=(0, 10), pady=(0, 8))ttk.Combobox(form, textvariable=country_var, values=["", "USA", "Canada", "UK", "Germany", "India"]).grid(row=4, column=1, sticky="ew", pady=(0, 8))# Row 5: Notes (expands)ttk.Label(form, text="Notes").grid(row=5, column=0, sticky="nw", padx=(0, 10), pady=(0, 8))notes = tk.Text(form, height=6, wrap="word")notes.grid(row=5, column=1, sticky="nsew", pady=(0, 8))# Row 6: Buttons aligned to the rightbuttons = ttk.Frame(form)buttons.grid(row=6, column=0, columnspan=2, sticky="e", pady=(6, 0))ttk.Button(buttons, text="Clear", command=clear).pack(side="right")ttk.Button(buttons, text="Save", command=submit).pack(side="right", padx=(0, 8))# Responsive behavior: make input column and notes row expandform.columnconfigure(0, weight=0)  # labels stay natural widthform.columnconfigure(1, weight=1)  # inputs expandform.rowconfigure(5, weight=1)     # notes row expands verticallyroot.mainloop()

What to test (and what each setting does)

  • Resize the window wider: the entries and notes expand because column 1 has weight=1 and widgets use sticky="ew"/"nsew".
  • Resize the window taller: the notes area grows because row 5 has weight=1 and the text widget uses sticky="nsew".
  • Buttons remain bottom-right within the form because their frame is placed in a full-width row with columnspan=2 and sticky="e".

Now answer the exercise about the content:

In a resizable Tkinter form built with grid(), what combination ensures that input fields grow wider and a multi-line Notes widget also grows taller when the window is resized?

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

You missed! Try again.

In grid(), window space is distributed using columnconfigure/rowconfigure weights, while sticky controls how widgets expand inside their cells. Use column weight for wider inputs and row weight plus sticky="nsew" for a vertically expanding Notes area.

Next chapter

Core Tkinter Widgets: Labels, Buttons, Entries, Text, and Variables

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