Side values in Tkinter's pack geometry manager

Last week we had a brief look at the pack geometry manager for Tkinter, which is a GUI development toolkit for Python. We ran into some problems when we looked at a piece of configuration for pack called side. In this post, we're going to take a closer look at pack, and we're going to figure out how exactly to use side, and what it can do for us.

If you're not very familiar with Tkinter, or you have no idea what it is at all, I'd recommend looking at last week's post. We cover some of the basics there. We also have a section on Tkinter in our Complete Python Course.

The story so far

We've been exploring pack using a simple fixed sized window and a couple of rectangular Label widgets. The code looks something like this:

import tkinter as tk

root = tk.Tk()
root.geometry("600x400")

rectangle_1 = tk.Label(root, text="Rectangle 1", bg="green", fg="white")
rectangle_1.pack(ipadx=10, ipady=10)

rectangle_2 = tk.Label(root, text="Rectangle 2", bg="red", fg="white")
rectangle_2.pack(ipadx=10, ipady=10)

root.mainloop()

Here, the only configuration we've added for pack is some internal padding to make the Label content easier to read.

Right now, the application looks like this:

We explored two pieces of configuration in the last post: fill and expand.

The former allows us to specify how much of a widget's allocated area the widget actually fills. In Tkinter, every widget is assigned some amount of space in the window, but the widget doesn't necessarily use all of it. For example, the Label widgets in the window above are horizontally centred in an area which stretches across the entire width of the application window.

One thing we did learn about fill is that it doesn't allow a widget to consume excess space in the window. We can't make either Label widget fill the height of the container, because the area below rectangle_2 is not allocated to those Label widgets.

If we want to allow widgets to fill this excess space, we need to provide an extra piece of configuration called expand, which accepts a Boolean value. With expand set to True, widgets will grow to fill any excess space in the application window.

For example, setting fill="both" and expand=True on rectangle_1 yields the following:

If more than one widget is has its expand configuration set to True, the widgets share the available space, even if the widgets don't fill that space:

However, when we set the side configuration on one of these widgets to "left", we saw that this rule didn't really hold up.

When we looked at this code:

import tkinter as tk

root = tk.Tk()
root.geometry("600x400")

rectangle_1 = tk.Label(root, text="Rectangle 1", bg="green", fg="white")
rectangle_1.pack(ipadx=10, ipady=10, side="left", expand=True, fill="both")

rectangle_2 = tk.Label(root, text="Rectangle 2", bg="red", fg="white")
rectangle_2.pack(ipadx=10, ipady=10, expand=True, fill="both")

root.mainloop()

We ended up with the following:

Instead of the available space being shared evenly, rectangle_1 took it all for itself.

We also discovered that if we set the side configuration of rectangle_2 to "left", everything went back to normal, albeit with the window split left to right, rather than top to bottom:

So, what's going on here?

Side basics

Before we look at how side interacts with with expand, let's look at some simpler examples.

side can take a number of different values, each of which is an edge or "side" of its container. In our case this is the application window. We can therefore provide any of "top", "bottom", "left", or "right" as values. By default, widgets have a side value of "top".

The normal use case for side is to change the placement of widgets in its container. For example, we can set side="bottom to have the widget anchored to the bottom of its container, rather than at the top. Using side in this way is fairly straight forward, but we run into issues when we combine different sides in a single container, especially when those widgets are allowed to expand.

Let's look at what happens when rectangle_1 is set to side="left" and both rectangles have their fill configuration set to "both". This will show us the default space allocated to each widget by pack.

import tkinter as tk

root = tk.Tk()
root.geometry("600x400")

rectangle_1 = tk.Label(root, text="Rectangle 1", bg="green", fg="white")
rectangle_1.pack(ipadx=10, ipady=10, side="left", fill="both")

rectangle_2 = tk.Label(root, text="Rectangle 2", bg="red", fg="white")
rectangle_2.pack(ipadx=10, ipady=10, fill="both")

root.mainloop()

The code above gives us a layout like this:

This is quite interesting, because we can see that rectangle_1 was allocated an area which stretches the entire height of the window, while rectangle_2 was allocated an area which stretches the width of the window.

We can also see that rectangle_1 was given precedence. We might be tempted to assume that side="left" is given precedence over the default side="top", since we've seen this in every case, but that would be a mistake. It's actually entirely down to the order in which items are added to the window.

If we move rectangle_1 below rectangle_2 in our code, we see the opposite:

import tkinter as tk

root = tk.Tk()
root.geometry("600x400")

rectangle_2 = tk.Label(root, text="Rectangle 2", bg="red", fg="white")
rectangle_2.pack(ipadx=10, ipady=10, fill="both")

rectangle_1 = tk.Label(root, text="Rectangle 1", bg="green", fg="white")
rectangle_1.pack(ipadx=10, ipady=10, side="left", fill="both")

root.mainloop()

If we add more rectangles to the application window, we can see very clearly that the order of precedence is entirely down to the ordering of the widgets:

import tkinter as tk

root = tk.Tk()
root.geometry("600x400")

rectangle_1 = tk.Label(root, text="Rectangle 1", bg="green", fg="white")
rectangle_1.pack(ipadx=10, ipady=10, side="left", fill="both")

rectangle_2 = tk.Label(root, text="Rectangle 2", bg="red", fg="white")
rectangle_2.pack(ipadx=10, ipady=10, fill="both")

rectangle_3 = tk.Label(root, text="Rectangle 3", bg="blue", fg="white")
rectangle_3.pack(ipadx=10, ipady=10, side="left", fill="both")

rectangle_4 = tk.Label(root, text="Rectangle 4", bg="yellow", fg="black")
rectangle_4.pack(ipadx=10, ipady=10, fill="both")

rectangle_5 = tk.Label(root, text="Rectangle 5", bg="orange", fg="white")
rectangle_5.pack(ipadx=10, ipady=10, side="left", fill="both")

root.mainloop()

So, how does this all work with expand in the mix?

Side with expand

Just as before, the order in which we add widgets to the window is very important for how side interacts with expand. We can see this very clearly in the following two examples.

First, we're going to set expand=True and fill="both" for both rectangles, and side="left" for rectangle_1.

import tkinter as tk

root = tk.Tk()
root.geometry("600x400")

rectangle_1 = tk.Label(root, text="Rectangle 1", bg="green", fg="white")
rectangle_1.pack(ipadx=10, ipady=10, expand=True, side="left", fill="both")

rectangle_2 = tk.Label(root, text="Rectangle 2", bg="red", fg="white")
rectangle_2.pack(ipadx=10, ipady=10, expand=True, fill="both")

root.mainloop()

This gives us a layout like this:

If we now switch things around so that rectangle_2 has side="left" instead, we end up with the following:

import tkinter as tk

root = tk.Tk()
root.geometry("600x400")

rectangle_1 = tk.Label(root, text="Rectangle 1", bg="green", fg="white")
rectangle_1.pack(ipadx=10, ipady=10, expand=True, fill="both")

rectangle_2 = tk.Label(root, text="Rectangle 2", bg="red", fg="white")
rectangle_2.pack(ipadx=10, ipady=10, expand=True, side="left", fill="both")

root.mainloop()

So what happens when we have more widgets?

Complex interfaces with side and expand

Let's go back to our five rectangle example, but this time, let's set some of the elements to expand instead.

import tkinter as tk

root = tk.Tk()
root.geometry("600x400")

rectangle_1 = tk.Label(root, text="Rectangle 1", bg="green", fg="white")
rectangle_1.pack(ipadx=10, ipady=10, expand=True, side="left", fill="both")

rectangle_2 = tk.Label(root, text="Rectangle 2", bg="red", fg="white")
rectangle_2.pack(ipadx=10, ipady=10, fill="both")

rectangle_3 = tk.Label(root, text="Rectangle 3", bg="blue", fg="white")
rectangle_3.pack(ipadx=10, ipady=10, expand=True, side="left", fill="both")

rectangle_4 = tk.Label(root, text="Rectangle 4", bg="yellow", fg="black")
rectangle_4.pack(ipadx=10, ipady=10, fill="both")

rectangle_5 = tk.Label(root, text="Rectangle 5", bg="orange", fg="white")
rectangle_5.pack(ipadx=10, ipady=10, expand=True, side="left", fill="both")

root.mainloop()

Now all of the odd numbered rectangles have expand=True as part of their pack configuration.

Interestingly, the space is still shared evenly between the widgets with side="left" as part of their configuration. The order in which we place widgets in the window doesn't affect how the space is shared between widgets with the same side configuration. This extends to parallel sides as well, so side="left" and side="right" are compatible for the purposes of sharing space.

However, when we have widgets with perpendicular side values, the order the widgets were added determines when a widget can claim space in the application window.

For example, if we add expand=True to every widget in the example above, we get the following:

Let's break this down.

rectangle_1 is the first widget placed in the application window, so it gets the entire height of the window allocated to it, and then expands into all of the available space. At this point, this is the entire application window.

When we add rectangle_2, it gets a narrow sliver on the right hand side of the application window, but it does fill up the entire height of the window.

Next we add rectangle_3 which has a side value of "left" and has been allowed to expand. Here a number of things happen. rectangle_1 is now forced to share its horizontal space with rectangle_3, which means it now only consumes half of the width of the application window. This means there is now more horizontal space for rectangle_2, so it fills up half the width of the window, and continues to take all the vertical space it can. It can no longer fill the entire height of the window, because rectangle_3 has taken space in the bottom right corner, but it still fills up most of the window height.

At this point, our window looks like this:

Next we add rectangle_4 which now forces rectangle_2 to share its vertical space. rectangle_4 slots in beside rectangle_3, taking up a narrow sliver of the horizontal space, but half of the vertical space. With rectangle_2 now sharing the vertical space, rectangle_3 now has space to grow in height, so rectangle_3 now also shares half the height of the application window.

We now have this:

Finally, we add rectangle_5 which causes a number of changes. It's another left aligned widget, so it forces both rectangle_1 and rectangle_3 to reduce the amount of extra space they claim in the horizontal direction. Each is now given just a third of the overall width.

This reduction in rectangle_1 and rectangle_3 allows rectangle_2 and rectangle_4 to also grow in width, with rectangle_2 claiming two thirds of the application window.

rectangle_5 now slots in underneath rectangle_4, giving us the result we saw earlier:

Wrapping up

That's it for this week's post on side values and pack! If you're interested in learning more about Tkinter and GUI development, check out our brand new course: GUI Development with Python and Tkinter.

You might want to also follow us on Twitter, as we'll be releasing more posts on Tkinter in the near future, including some mini-project walkthroughs.