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, orbothto stretch in that direction.expand: ifTrue, 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 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
rowconfigureandcolumnconfigureweights 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
Framewithpaddingfor 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 andsticky="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 herestatus_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=1and widgets usesticky="ew"/"nsew". - Resize the window taller: the notes area grows because row 5 has
weight=1and the text widget usessticky="nsew". - Buttons remain bottom-right within the form because their frame is placed in a full-width row with
columnspan=2andsticky="e".