Previous | Tutorial index | Next
pack, grid, and place.Imagine you are an architect designing a room. You need to decide where the furniture goes. Do you:
pack() – simple stacking).grid() – table layout).place() – absolute positioning).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.
pack() – The Stackerpack() 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.
widget.pack(options)
| 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). |
anchor setting.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()
TOP and BOTTOM stack vertically.LEFT and RIGHT stack horizontally.fill and expandfill=tk.X makes the widget stretch to fill the entire width of its container.expand=True tells Tkinter: "If there is unused space in the container after placing all widgets, give it to this widget."pack(side=tk.BOTTOM, fill=tk.X).pack(expand=True, fill=tk.BOTH).pack()grid() – The Table Mastergrid() 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.
widget.grid(options)
| 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. |
columnconfigure and rowconfigureThis 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)
weight – A number that allocates extra space. If two columns have weight=1, they share space equally. If one has weight=2, it gets twice as much extra space.minsize – Sets a minimum size for the row/column.uniform – Groups columns together so they maintain the same width.stickysticky="w" – left align within the cell.sticky="e" – right align.sticky="n" – top align.sticky="s" – bottom align.sticky="ew" – stretch horizontally (equivalent to fill=tk.X in pack).sticky="nsew" – stretch both ways (equivalent to fill=tk.BOTH, expand=True in pack).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()
grid()place() – The Manual Measurerplace() 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.
widget.place(options)
| 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. |
place and propagateWhen 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)
place()place do not respond well to window resizing and are difficult to maintain.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).
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()
pack or grid to divide into major regions.Ask yourself these questions:
pack().grid().place(), but only for small details.grid() with columnconfigure/rowconfigure weights is your best friend. pack() with expand=True is also good. place() is terrible for resizing.rowspan and columnspan?
pack()grid()place()pack() options achieve this?
expand=True, fill=tk.BOTHfill=tk.Xfill=tk.Yside=tk.TOP, expand=Truegrid()?
root.grid_column(0, expand=True)root.columnconfigure(0, weight=1)root.rowconfigure(0, weight=1)col0.pack(expand=True)pack() and grid() on two different children of the same parent?
place().place() allows you to position a widget exactly in the center of its parent (50% from left, 50% from top)?
x=0.5, y=0.5relx=0.5, rely=0.5center=Truepos="center"pack() manager can only stack widgets vertically.grid() requires you to specify a row and column for every widget.place() inside a Frame, the Frame will automatically resize to contain the placed widget.pack() in the root window and grid() inside a child Frame that is packed into the root.sticky="nsew" option in grid() is equivalent to pack(expand=True, fill=tk.BOTH).pack, you use ________ and ________.grid() option that specifies which side of the cell a widget sticks to is called ________.Frame from shrinking to fit its children when using place, you call frame.____________(False).pack() option that determines whether a widget takes up extra space in the container is ________.grid(), if you want a widget to occupy two columns, you set columnspan=____.Create a window that simulates a document editor toolbar:
Label) that stretches fully horizontally at the bottom, with the text "Ready".pack().Using grid(), create the layout for a simple calculator:
Entry widget spanning the top (all 4 columns) for the display.columnconfigure with weight=1 and rowconfigure with weight=1 for all).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).
Design a simple contact manager layout:
pack(side=tk.TOP).grid to create a form with labels and entries (Name, Phone, Email).pack(side=tk.BOTTOM, fill=tk.X) containing three buttons: "Add", "Edit", "Delete" packed side by side (side=tk.LEFT).Demonstrate your ability to choose, implement, and combine geometry managers to create a complex, well‑structured GUI layout that handles resizing gracefully.
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.
pack(side=tk.TOP, fill=tk.X) to place a Frame. Inside it, create a Menu widget (using root.config(menu=...) – this is separate from geometry managers). This is free marks.Frame taking up ~60% of the width. Use pack(side=tk.LEFT, expand=True, fill=tk.BOTH). Inside, place a Listbox and a vertical Scrollbar linked to it. (Use pack(side=tk.LEFT, fill=tk.BOTH, expand=True) for the Listbox, and pack(side=tk.RIGHT, fill=tk.Y) for the Scrollbar).Frame taking up ~40% of the width. Use pack(side=tk.RIGHT, expand=True, fill=tk.BOTH). Inside this right frame, use grid to place:
ew)ew)ew)columnconfigure(1, weight=1)). Configure the row weights so the content stays at the top.Frame using pack(side=tk.BOTTOM, fill=tk.X). Place 5 buttons in this frame: "Previous", "Play", "Pause", "Stop", "Next". Use pack(side=tk.LEFT, padx=5, pady=5) for each.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.
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.
Errors identified:
Mixing grid() and pack() in the same parent (root).
Consequence: The program will freeze or crash because Tkinter cannot resolve conflicting layout algorithms.
Attempting to use grid() after pack() has been used on the same parent.
Consequence: Same as above – freeze/crash.
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.
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().
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.
Write a script that contains a "Toggle Layout" button.
pack(side=tk.TOP)).pack(side=tk.LEFT)).pack_forget() on both labels before re‑packing them.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.
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.
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.
| 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). |
rowconfigure and columnconfigure weights—critical for professional apps.grid‑only layout, then a pack‑only layout. Which was easier? This will cement your understanding of their strengths.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.