Structuring a Tkinter application
One of the main advantages of making applications with Tkinter is that it is very easy to set up a basic GUI with a script of a few lines. As the programs get more complex, it becomes more difficult to separate logically each part, so an organized structure will help us to keep our code clean.
Getting ready
We will take the following program as an example:
from tkinter import * root = Tk() btn = Button(root, text="Click me!") btn.config(command=lambda: print("Hello, Tkinter!")) btn.pack(padx=120, pady=30) root.title("My Tkinter app") root.mainloop()
It creates a main window with a button that prints Hello, Tkinter!
in the console each time it is clicked. The button is placed with a padding of 120px in the horizontal axis and 30px in the vertical axis. The last statement starts the main loop, which processes user events and updates the GUI until the main window is destroyed:

You can execute the program and verify that it is working as expected. However, all our variables are defined in the global namespace, and the more widgets you add, the more difficult it becomes to reason about the parts where they are used.
Note
Wildcard imports (from ... import *
) are strongly discouraged in production code because they pollute your global namespace—we only used them here to illustrate an anti-pattern that can be commonly seen in online examples.
These maintainability issues can be addressed with basic OOP techniques, which are considered good practice in all types of Python programs.
How to do it...
To improve the modularity of our simple program, we will define a class that wraps our global variables:
import tkinter as tk class App(tk.Tk): def __init__(self): super().__init__() self.btn = tk.Button(self, text="Click me!", command=self.say_hello) self.btn.pack(padx=120, pady=30) def say_hello(self): print("Hello, Tkinter!") if __name__ == "__main__": app = App() app.title("My Tkinter app") app.mainloop()
Now, each variable is enclosed in a specific scope, including the command
function, which is moved as a separate method.
How it works...
First, we replaced the wildcard import with the import ... as
syntax to have better control over our global namespace.
Then, we defined our App
class as a Tk
subclass, which now is referenced via the tk
namespace. To properly initialize the base class, we will call the __init__
method of the Tk
class with the built-in super()
function. This corresponds to the following lines:
class App(tk.Tk): def __init__(self): super().__init__() # ...
Now, we have a reference to the App
instance with the self
variable, so we will add all the Button widget as an attribute of our class.
Although it may look overkill for such a simple program, this refactoring will help us to reason about each part, the button instantiation is separated from the callback that gets executed when it is clicked, and the application bootstrapping is moved to the if __name__ == "__main__"
block, which is a common practice in executable Python scripts.
We will follow this convention through all the code samples, so you can take this template as the starting point of any larger application.
There's more...
We subclassed the Tk
class in our example, but it is also common to subclass other widget classes. We did this to reproduce the same statements that we had before we refactored the code.
However, it may be more convenient to subclass Frame
or Toplevel
in larger programs, such as those with multiple windows. This is because a Tkinter application should have only one Tk
instance, and the system creates one automatically if you instantiate a widget before you create the Tk
instance.
Keep in mind that this decision does not affect the structure of our App
class since all widget classes have a mainloop
method that internally starts the Tk
main loop.