Previous | Tutorial index | Next

Tutorial 5: Tkinter Geometry Managers – Layout Your GUI

Learning Objectives

1. Introduction – Why Do We Need Geometry Managers?

Imagine you are an architect designing a room. You need to decide where the furniture goes. Do you:

Tkinter gives you these three tools—geometry managers—to arrange widgets inside their parent containers. Each tool has a different philosophy, strengths, and weaknesses.

A critical rule to remember: A single parent container (e.g., a Frame or the root window) can only use ONE geometry manager at a time. If you try to use pack() on one child and grid() on another child of the same parent, your program will freeze or crash. However, you can use different managers in different containers (e.g., pack() in the root, grid() inside a child frame).

Let's dive deep into each manager.

2. Geometry Manager 1: pack() – The Stacker

2.1 How It Works

pack() treats the parent container like a spring-loaded box. It allocates a slice of space to a widget and places it along a specified side. The widgets are "packed" into the container one after another, like books on a shelf.

2.2 Syntax

widget.pack(options)

2.3 Core Options Explained

Option Values Description
side tk.TOP (default), tk.BOTTOM, tk.LEFT, tk.RIGHT Which side of the container to pack against.
fill tk.NONE (default), tk.X, tk.Y, tk.BOTH Whether the widget should expand to fill extra space horizontally/vertically.
expand True or False (default) If True, the widget takes up any extra space that is left in the container.
padx / pady Integer (pixels) External padding outside the widget's border.
ipadx / ipady Integer (pixels) Internal padding inside the widget's border (increases the widget size).
anchor tk.N, tk.S, tk.E, tk.W, tk.CENTER, etc. Where the widget sits within its allocated parcel of space (if the parcel is larger than the widget).
before / after Another widget object Packs the widget before or after another existing widget (used for dynamic insertion).

2.4 The Packing Algorithm (Behind the Scenes)

  1. Allocate space – Tkinter determines which side the widget is going to.
  2. Claim area – It claims the requested amount of space along that side.
  3. Centre/Anchor – If the allocated area is larger than the widget's requested size, the widget is placed inside using the anchor setting.
  4. Repeat – The next widget is packed into the remaining space.

2.5 Visual Example: Side-by-Side Comparison

import tkinter as tk root = tk.Tk() # Default TOP stacking (vertical) btn1 = tk.Button(root, text="Top") btn1.pack(side=tk.TOP) # Default btn2 = tk.Button(root, text="Bottom") btn2.pack(side=tk.BOTTOM) btn3 = tk.Button(root, text="Left") btn3.pack(side=tk.LEFT) btn4 = tk.Button(root, text="Right") btn4.pack(side=tk.RIGHT) root.mainloop()

2.6 Mastering fill and expand

2.7 When to Use pack()

3. Geometry Manager 2: grid() – The Table Master

3.1 How It Works

grid() treats the container as a spreadsheet divided into rows and columns. You place widgets into specific cells. This is the most versatile and widely used manager for complex forms and applications.

3.2 Syntax

widget.grid(options)

3.3 Core Options Explained

Option Values Description
row / column Integer (0-indexed) The cell position. Default is 0.
rowspan / columnspan Integer How many rows/columns this widget should span (like merging cells in Excel).
sticky String combination of "n", "s", "e", "w" (e.g., "nsew") Which sides of the cell the widget "sticks" to. Works like fill and anchor combined. "ew" stretches horizontally, "ns" vertically, "nsew" fills the whole cell.
padx / pady Integer External padding around the widget inside its cell.
ipadx / ipady Integer Internal padding inside the widget.
columnspan / rowspan Integer Merges multiple cells for a single widget.

3.4 The Most Overlooked Feature: columnconfigure and rowconfigure

This is critical for responsive GUIs. By default, rows and columns do NOT expand when the window is resized. You must explicitly tell Tkinter which rows/columns should grow to fill extra space using weight.

# Make column 1 expand horizontally and row 1 expand vertically root.columnconfigure(1, weight=1) root.rowconfigure(1, weight=1)

3.5 Mastering sticky

3.6 Form Example (Login Screen)

import tkinter as tk root = tk.Tk() # Configure weights so the grid expands root.columnconfigure(0, weight=1) root.columnconfigure(1, weight=3) root.rowconfigure(0, weight=1) root.rowconfigure(1, weight=1) # Labels on the left (sticky E to align right) tk.Label(root, text="Username:").grid(row=0, column=0, sticky="e", padx=5, pady=5) tk.Label(root, text="Password:").grid(row=1, column=0, sticky="e", padx=5, pady=5) # Entries on the right (sticky EW to fill horizontally) entry_user = tk.Entry(root) entry_pass = tk.Entry(root, show="*") entry_user.grid(row=0, column=1, sticky="ew", padx=5, pady=5) entry_pass.grid(row=1, column=1, sticky="ew", padx=5, pady=5) # Submit button spanning 2 columns, centered btn = tk.Button(root, text="Login") btn.grid(row=2, column=0, columnspan=2, pady=10) root.mainloop()

3.7 When to Use grid()

4. Geometry Manager 3: place() – The Manual Measurer

4.1 How It Works

place() allows you to specify the exact pixel coordinates (or relative percentages) of a widget within its container. It gives you total control but sacrifices automatic resizing.

4.2 Syntax

widget.place(options)

4.3 Core Options Explained

Option Values Description
x / y Integer (pixels) Absolute position of the widget's top‑left corner (unless anchor changes).
relx / rely Float (0.0 to 1.0) Relative position within the parent (0.0 = left/top edge, 1.0 = right/bottom edge).
anchor tk.N, tk.S, tk.E, tk.W, tk.CENTER, etc. Which part of the widget is positioned at the (x, y) coordinate.
width / height Integer Absolute size in pixels.
relwidth / relheight Float Relative size (e.g., 0.5 means 50% of the parent's width).
bordermode "inside" or "outside" Whether to include the parent's border in relative calculations.

4.4 Important Caveat: place and propagate

When using place, the parent container often does not expand to fit the child because the child is not "managed" by pack/grid. To set a fixed size for a frame that uses place, you must disable propagate:

frame = tk.Frame(root, width=200, height=100, bg="lightgrey") frame.pack_propagate(False) # Do not shrink to fit children frame.pack() label = tk.Label(frame, text="Fixed size frame") label.place(relx=0.5, rely=0.5, anchor=tk.CENTER)

4.5 When to Use place()

5. The Golden Rule – Mixing Managers

5.1 Why Can't You Mix pack() and grid() in the Same Parent?

Tkinter's geometry managers are mutually exclusive on a per‑container basis. Each container uses a specific algorithm to calculate the size and position of its children.

When you call pack() on a child, the parent starts using the pack algorithm. If you then call grid() on another child of the same parent, Tkinter's internal layout engine gets confused—it is receiving conflicting size requests from two different solvers, leading to an infinite loop (a freeze).

5.2 The Safe Way – Nesting Frames

You can use different managers in different containers. This is the standard pattern for complex GUIs:

# Root uses pack to split the screen into top and bottom root = tk.Tk() top_frame = tk.Frame(root) bottom_frame = tk.Frame(root) top_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True) bottom_frame.pack(side=tk.BOTTOM, fill=tk.X) # Top frame uses grid tk.Label(top_frame, text="Name:").grid(row=0, column=0) tk.Entry(top_frame).grid(row=0, column=1) # Bottom frame uses pack (for a status bar/toolbar) tk.Button(bottom_frame, text="Save").pack(side=tk.LEFT) tk.Button(bottom_frame, text="Cancel").pack(side=tk.RIGHT) root.mainloop()

5.3 Summary of Nesting Strategy

  1. Root Window: Use pack or grid to divide into major regions.
  2. Sub‑Frames: Use whichever manager best fits the layout inside that region.
  3. Remember: The root is also a parent container – the same rule applies to it.

6. Choosing the Right Manager – A Decision Guide

Ask yourself these questions:

Quiz 1: Multiple Choice

  1. Which geometry manager allows you to merge cells using rowspan and columnspan?
AnswerB – `grid()` uses rowspan and columnspan.
  1. You want a widget to stretch horizontally to fill the entire width of its parent, but keep its original height. Which pack() options achieve this?
AnswerB – `fill=tk.X` stretches horizontally.
  1. What is the correct way to make column 0 expand when the window is resized using grid()?
AnswerB – `columnconfigure(weight=1)` configures the column's expansion weight.
  1. What happens if you call pack() and grid() on two different children of the same parent?
AnswerC – This violates the golden rule and causes a freeze.
  1. Which option in place() allows you to position a widget exactly in the center of its parent (50% from left, 50% from top)?
AnswerB – `relx=0.5, rely=0.5` with `anchor=tk.CENTER` centers the widget.

Quiz 2: True or False

  1. True / False: The pack() manager can only stack widgets vertically.
AnswerFalse – It can stack in four directions (`TOP`, `BOTTOM`, `LEFT`, `RIGHT`).
  1. True / False: Using grid() requires you to specify a row and column for every widget.
AnswerFalse – You can omit them; default is `0, 0`.
  1. True / False: If you use place() inside a Frame, the Frame will automatically resize to contain the placed widget.
AnswerFalse – Frames do not propagate size for `place`; you must set `propagate(False)` or explicit sizes.
  1. True / False: You can use pack() in the root window and grid() inside a child Frame that is packed into the root.
AnswerTrue – Different containers = different managers allowed.
  1. True / False: The sticky="nsew" option in grid() is equivalent to pack(expand=True, fill=tk.BOTH).
AnswerTrue – Both options make the widget fill all available space in its container/cell.

Quiz 3: Fill in the Blanks

  1. To add external space between widgets in pack, you use ________ and ________.
Answer`padx`, `pady`
  1. The grid() option that specifies which side of the cell a widget sticks to is called ________.
Answersticky
  1. To prevent a Frame from shrinking to fit its children when using place, you call frame.____________(False).
Answer`pack_propagate`
  1. The pack() option that determines whether a widget takes up extra space in the container is ________.
Answer`expand`
  1. In grid(), if you want a widget to occupy two columns, you set columnspan=____.
Answer`2`

8. Hands‑On Lab (In‑Class Exercises)

Exercise 1: Pack Practice – Toolbar

Create a window that simulates a document editor toolbar:

Sample solution ```python import tkinter as tk root = tk.Tk() top = tk.Frame(root) top.pack(side=tk.TOP, fill=tk.X) tk.Button(top, text="New").pack(side=tk.LEFT) tk.Button(top, text="Open").pack(side=tk.LEFT) tk.Button(top, text="Save").pack(side=tk.LEFT) tk.Button(top, text="Settings").pack(side=tk.RIGHT) bottom = tk.Label(root, text="Ready", relief=tk.SUNKEN) bottom.pack(side=tk.BOTTOM, fill=tk.X) root.mainloop() ```

Exercise 2: Grid Practice – Calculator Layout

Using grid(), create the layout for a simple calculator:

Sample solution outline Create root, set columnconfigure(0..3, weight=1) and rowconfigure(0..4, weight=1). Place Entry at row0, columnspan=4, sticky="ew". Then loop over buttons, place at rows 1‑4, columns 0‑3. Use sticky="nsew" on each button.

Exercise 3: Place Practice – Splash Screen Overlay

Create a 300x200 Frame with a light blue background. Inside it, use place() to put a "Loading..." Label exactly in the center of the frame. Also, place a small red square (a Label with bg="red", width=5, height=5) at the bottom‑right corner (relx=1.0, rely=1.0, anchor=tk.SE).

Sample solution ```python frame = tk.Frame(root, width=300, height=200, bg="lightblue") frame.pack_propagate(False) frame.pack() label = tk.Label(frame, text="Loading...", font=("Arial", 16)) label.place(relx=0.5, rely=0.5, anchor=tk.CENTER) square = tk.Label(frame, bg="red", width=5, height=5) square.place(relx=1.0, rely=1.0, anchor=tk.SE) ```

Exercise 4: Nesting Frames – The Kitchen Sink

Design a simple contact manager layout:

  1. Top Frame: A title "My Contacts" (Label) – use pack(side=tk.TOP).
  2. Middle Frame (Main): Use grid to create a form with labels and entries (Name, Phone, Email).
  3. Bottom Frame: Use pack(side=tk.BOTTOM, fill=tk.X) containing three buttons: "Add", "Edit", "Delete" packed side by side (side=tk.LEFT).
Sample solution outline Root with pack: top_label.pack(side=tk.TOP), mid_frame.pack(expand=True, fill=tk.BOTH), bottom_frame.pack(side=tk.BOTTOM, fill=tk.X). In mid_frame, use grid for labels/entries. In bottom_frame, pack buttons side=LEFT.
Here is the revised homework assignment from Tutorial 5, now including sample answers for each part using HTML `
` tags. The assignment instructions remain unchanged; the answers are hidden and can be revealed by the student.

9. Homework Assignment

Objective

Demonstrate your ability to choose, implement, and combine geometry managers to create a complex, well‑structured GUI layout that handles resizing gracefully.

Part A: Build a Music Player Interface (20 points)

Build a Python script that replicates the basic layout of a desktop music player. Do not worry about actual music playback – focus only on the GUI layout.

Sample Solution for Part A
import tkinter as tk root = tk.Tk() root.title("My Music Player") root.geometry("600x400") root.resizable(True, True) # ---- Top Menu Bar ---- # The menu bar is attached to root, not a frame, but we create a frame for visual consistency (optional) menu_frame = tk.Frame(root, height=20) menu_frame.pack(side=tk.TOP, fill=tk.X) menubar = tk.Menu(root) root.config(menu=menubar) file_menu = tk.Menu(menubar, tearoff=0) menubar.add_cascade(label="File", menu=file_menu) file_menu.add_command(label="Open") file_menu.add_command(label="Exit", command=root.destroy) # ---- Middle Section ---- middle_frame = tk.Frame(root) middle_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True) # Left frame (Playlist) – 60% width left_frame = tk.Frame(middle_frame) left_frame.pack(side=tk.LEFT, expand=True, fill=tk.BOTH) listbox = tk.Listbox(left_frame) listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) scrollbar = tk.Scrollbar(left_frame, orient=tk.VERTICAL, command=listbox.yview) scrollbar.pack(side=tk.RIGHT, fill=tk.Y) listbox.config(yscrollcommand=scrollbar.set) # Right frame (Track Info) – 40% width right_frame = tk.Frame(middle_frame) right_frame.pack(side=tk.RIGHT, expand=True, fill=tk.BOTH) # Configure grid columns/rows for resizing right_frame.columnconfigure(0, weight=1) # label column right_frame.columnconfigure(1, weight=3) # entry column right_frame.rowconfigure(0, weight=0) # title row stays at top right_frame.rowconfigure(1, weight=1) right_frame.rowconfigure(2, weight=1) right_frame.rowconfigure(3, weight=1) # Now playing label lbl_now = tk.Label(right_frame, text="Now Playing", font=("Arial", 12, "bold")) lbl_now.grid(row=0, column=0, columnspan=2, sticky="w", padx=5, pady=5) # Title tk.Label(right_frame, text="Title:").grid(row=1, column=0, sticky="e", padx=5, pady=5) entry_title = tk.Entry(right_frame) entry_title.grid(row=1, column=1, sticky="ew", padx=5, pady=5) # Artist tk.Label(right_frame, text="Artist:").grid(row=2, column=0, sticky="e", padx=5, pady=5) entry_artist = tk.Entry(right_frame) entry_artist.grid(row=2, column=1, sticky="ew", padx=5, pady=5) # Duration tk.Label(right_frame, text="Duration:").grid(row=3, column=0, sticky="e", padx=5, pady=5) entry_duration = tk.Entry(right_frame) entry_duration.grid(row=3, column=1, sticky="ew", padx=5, pady=5) # ---- Bottom Playback Controls ---- bottom_frame = tk.Frame(root) bottom_frame.pack(side=tk.BOTTOM, fill=tk.X) buttons = ["Previous", "Play", "Pause", "Stop", "Next"] for btn_text in buttons: btn = tk.Button(bottom_frame, text=btn_text) btn.pack(side=tk.LEFT, padx=5, pady=5) root.mainloop()

This solution builds the required layout with proper nesting and resizing behaviour. The left Listbox expands in both directions because its parent left_frame is packed with expand=True, fill=tk.BOTH, and the listbox itself is packed the same way. The right frame's entries expand horizontally due to columnconfigure(1, weight=1). The bottom frame stays fixed at the bottom because it is packed with side=tk.BOTTOM.

Part B: Diagnosing and Fixing Layout Errors (10 points)

A student submitted the following code to create a login screen. The code contains three distinct violations of the geometry management rules. Identify each error, explain the exact consequence, and provide the corrected code.

import tkinter as tk root = tk.Tk() root.title("Login") # Error 1: Mixing managers in the root label = tk.Label(root, text="Username:") label.grid(row=0, column=0) entry = tk.Entry(root) entry.pack(pady=5) def on_submit(): print("Submitted") # Error 2: Trying to grid a button after pack was used on the root btn = tk.Button(root, text="Submit", command=on_submit) btn.grid(row=1, column=0) # Error 3: Forgetting to set row/column weights (not a freeze, but bad design) root.mainloop()

After fixing the freeze errors, add the necessary columnconfigure and rowconfigure lines to make the entry expand horizontally and the button remain centered at the bottom when the window is resized.

Sample Solution for Part B

Errors identified:

  1. Mixing grid() and pack() in the same parent (root).
    Consequence: The program will freeze or crash because Tkinter cannot resolve conflicting layout algorithms.

  2. Attempting to use grid() after pack() has been used on the same parent.
    Consequence: Same as above – freeze/crash.

  3. No columnconfigure or rowconfigure to allow resizing.
    Consequence: When the window is resized, the widgets will not expand or move appropriately, leaving the layout unresponsive.

Corrected code:

import tkinter as tk root = tk.Tk() root.title("Login") root.geometry("300x150") root.resizable(True, True) # Use grid consistently root.columnconfigure(0, weight=1) root.columnconfigure(1, weight=1) root.rowconfigure(0, weight=1) root.rowconfigure(1, weight=1) root.rowconfigure(2, weight=1) # Row 0: label and entry label = tk.Label(root, text="Username:") label.grid(row=0, column=0, sticky="e", padx=5, pady=5) entry = tk.Entry(root) entry.grid(row=0, column=1, sticky="ew", padx=5, pady=5) # Row 1: submit button centered (spanning two columns) def on_submit(): print("Submitted") btn = tk.Button(root, text="Submit", command=on_submit) btn.grid(row=1, column=0, columnspan=2, pady=10) root.mainloop()

This fixes the freeze by using only grid(). The columnconfigure weights allow the entry to expand horizontally, and the button is centred by spanning both columns.

Part C: The place() Trap – Critical Analysis (5 points)

Write a short paragraph (5-7 sentences) explaining why place() is rarely recommended for full application layouts. Use specific arguments such as: window resizing, screen resolution differences, font size changes, and maintainability. Give a concrete example of a widget behavior that would break if place() were used instead of grid() or pack().

Sample Answer for Part C

place() is rarely used for full application layouts because it relies on absolute or relative coordinates that do not adapt well to dynamic changes. When the user resizes the window, widgets placed with place() do not automatically reposition or resize unless explicit calculations are made, leading to empty spaces or overlapping elements. Different screen resolutions and DPI scaling further break absolute positioning, as pixel coordinates do not translate consistently across displays. Additionally, if the user changes the system font size, widgets sized with place() may not accommodate the larger text, causing clipping or misalignment. Maintaining a place‑based layout is also cumbersome because every widget's position must be manually updated when new widgets are added. For example, a button placed 50 pixels from the top will stay there even if a new row of widgets is inserted above it, unlike grid() which can automatically shift rows. This rigidity makes place() unsuitable for responsive, maintainable applications.

Part D: Dynamic Packing (5 points)

Write a script that contains a "Toggle Layout" button.

Sample Solution for Part D
import tkinter as tk root = tk.Tk() root.title("Dynamic Packing") root.geometry("300x150") label1 = tk.Label(root, text="Label 1", bg="lightblue", width=10) label2 = tk.Label(root, text="Label 2", bg="lightgreen", width=10) # Initial vertical layout label1.pack(side=tk.TOP, padx=5, pady=5) label2.pack(side=tk.TOP, padx=5, pady=5) def toggle_layout(): # Forget both labels (remove from current layout) label1.pack_forget() label2.pack_forget() # Determine new side based on current layout state # We can use a variable to track layout mode, or simply check the side of label1 # For simplicity, we'll use a global flag global vertical if vertical: # Switch to horizontal label1.pack(side=tk.LEFT, padx=5, pady=5) label2.pack(side=tk.LEFT, padx=5, pady=5) else: # Switch to vertical label1.pack(side=tk.TOP, padx=5, pady=5) label2.pack(side=tk.TOP, padx=5, pady=5) vertical = not vertical vertical = True btn = tk.Button(root, text="Toggle Layout", command=toggle_layout) btn.pack(side=tk.BOTTOM, pady=10) root.mainloop()

This script uses a flag vertical to track the current orientation. The pack_forget() method removes the widgets from the layout without destroying them, allowing them to be re‑packed with new options.

Part E: Reflection on Nested Frames (5 points)

In your own words (6-8 sentences), explain the concept of "nesting frames" and why it is the standard solution for building complex GUIs in Tkinter. Use the Music Player (Part A) as a concrete example to illustrate how nesting frames allowed you to use grid in one area and pack in another without violating the golden rule.

Sample Answer for Part E

Nesting frames means placing Frame widgets inside other containers to create a hierarchical structure, where each frame can have its own geometry manager independent of its parent. This is the standard approach for complex GUIs because it allows you to mix pack(), grid(), and even place() in different parts of the application without violating the golden rule (never mix managers in the same container). In the Music Player, the root window uses pack() to arrange the top menu, middle content, and bottom controls. Inside the middle section, we nested a left frame (using pack() for the listbox and scrollbar) and a right frame (using grid() for the track information). Because these frames are distinct containers, the pack() used in the left frame and the grid() used in the right frame never conflict. Nesting frames also makes the code more modular and easier to maintain, as each section can be developed and debugged independently.

10. Summary of Key Terms (Glossary)

Term Definition
Geometry Manager The layout engine (pack, grid, place) that determines the size/position of widgets.
Parent Container The widget that holds children. The geometry manager is applied to the parent.
Weight (grid) A numeric value that determines how much extra space a row/column receives during resizing.
Sticky A grid option (compass directions) that controls how a widget sticks to the edges of its cell.
Expand (pack) A Boolean that tells Tkinter to allocate unused space to the widget.
Fill (pack) An option that stretches the widget within its allocated space (X, Y, or BOTH).
Nesting The practice of placing Frame widgets inside other containers to allow different geometry managers on different "levels".
Propagate When False, a container does not resize itself to fit its children—useful for place.
Grid Forgetting The grid_remove() method hides a widget without destroying it (preserves its grid configuration).

11. Further Resources for Self‑Study

This tutorial is designed to take approximately 2.5–3 hours of study, lab work, and homework. Mastering geometry managers is arguably the most important step in Tkinter—once you understand them, you can design any interface imaginable.

Previous | Tutorial index | Next