Python supports object-oriented programming (OOP) through classes, which allow you to bundle data and behavior in a single entity. Python classes allow you to quickly model concepts by creating representations of real objects that you can then use to organize your code.
Most of the currently available GUI frameworks for Python developers, such as PyQt, PySide, and Tkinter, rely on classes to provide apps, windows, widgets, and more. This means that you'll be actively using classes for designing and developing your GUI apps.
In this tutorial, you'll learn how OOP and classes work in Python. This knowledge will allow you to quickly grasp how GUI frameworks are internally organized, how they work, and how you can use their classes and APIs to create robust GUI applications.
Defining Classes in Python
Python classes are templates or blueprints that allow us to create objects through instantiation. These objects will contain data representing the object's state, and methods that will act on the data providing the object's behavior.
Instantiation is the process of creating instances of a class by calling the class constructor with appropriate arguments.
Attributes and methods make up what is known as the class interface or API. This interface allows us to operate on the objects without needing to understand their internal implementation and structure.
Alright, it is time to start creating our own classes. We'll start by defining a Color
class with minimal functionality. To do that in Python, you'll use the class
keyword followed by the class name. Then you provide the class body in the next indentation level:
Create GUI Applications with Python & Qt6 by Martin Fitzpatrick — (PySide6 Edition) The hands-on guide to making apps with Python — Over 10,000 copies sold!
>>> class Color:
... pass
...
>>> red = Color()
>>> type(red)
<class '__main__.Color'>
In this example, we defined our Color
class using the class
keyword. This class is empty. It doesn't have attributes or methods. Its body only contains a pass
statement, which is Python's way to do nothing.
Even though the class is minimal, it allows us to create instances by calling its constructor, Colo()
. So, red
is an instance of Color
. Now let's make our Color
class more fun by adding some attributes.
Adding Class and Instance Аttributes
Python classes allow you to add two types of attributes. You can have class and instance attributes. A class attribute belongs to its containing class. Its data is common to the class and all its instances. To access a class attribute, we can use either the class or any of its instances.
Let's now add a class attribute to our Color
class. For example, let's say we need to keep note of how many instance of Color
your code creates. Then you can have a color_count
attribute:
>>> class Color:
... color_count = 0
... def __init__(self):
... Color.color_count += 1
...
>>> red = Color()
>>> green = Color()
>>> Color.color_count
2
>>> red.color_count
2
Now Color
has a class attribute called color_count
that gets incremented every time we create a new instance. We can quickly access that attribute using either the class directly or one of its instances, like red
.
To follow up with this example, say that we want to represent our Color
objects using red, green, and blue attributes as part of the RGB color model. These attributes should have specific values for specific instances of the class. So, they should be instance attributes.
To add an instance attribute to a Python class, you must use the .__init__()
special method, which we introduced in the previous code but didn't explain. This method works as the instance initializer because it allows you to provide initial values for instance attributes:
>>> class Color:
... color_count = 0
... def __init__(self, red, green, blue):
... Color.color_count += 1
... self.red = red
... self.green = green
... self.blue = blue
...
>>> red = Color(255, 0, 0)
>>> red.red
255
>>> red.green
0
>>> red.blue
0
>>> Color.red
Traceback (most recent call last):
...
AttributeError: type object 'Color' has no attribute 'red'
Cool! Now our Color
class looks more useful. It has the usual class attributes and also three new instance attributes. Note that, unlike class attributes, instance attributes can't be accessed through the class itself. They're specific to a concrete instance.
There's something that jumps into sight in this new version of Color
. What is the self
argument in the definition of .__init__()
? This attribute holds a reference to the current instance. Using the name self
to identify the current instance is a strong convention in Python.
We'll use self
as the first or even the only argument to instance methods like .__init__()
. Inside an instance method, we'll use self
to access other methods and attributes defined in the class. To do that, we must prepend self
to the name of the target attribute or method instance of the class.
For example, our class has an attribute .red
that we can access using the syntax self.red
inside the class. This will return the number stored under that name. From outside the class, you need to use a concrete instance instead of self
.
Providing Behavior With Methods
A class bundles data (attributes) and behavior (methods) together in an object. You'll use the data to set the object's state and the methods to operate on that data or state.
Methods are just functions that we define inside a class. Like functions, methods can take arguments, return values, and perform different computations on an object's attributes. They allow us to make our objects usable.
In Python, we can define three types of methods in our classes:
- Instance methods, which need the instance (
self
) as their first argument - Class methods, which take the class (
cls
) as their first argument - Static methods, which take neither the class nor the instance
Let's now talk about instance methods. Say that we need to get the attributes of our Color
class as a tuple of numbers. In this case, we can add an .as_tuple()
method like the following:
class Color:
representation = "RGB"
def __init__(self, red, green, blue):
self.red = red
self.green = green
self.blue = blue
def as_tuple(self):
return self.red, self.green, self.blue
This new method is pretty straightforward. Since it's an instance method, it takes self
as its first argument. Then it returns a tuple containing the attributes .red
, .green
, and .blue
. Note how you need to use self
to access the attributes of the current instance inside the class.
This method may be useful if you need to iterate over the RGB components of your color objects:
>>> red = Color(255, 0, 0)
>>> red.as_tuple()
(255, 0, 0)
>>> for level in red.as_tuple():
... print(level)
...
255
0
0
Our as_tuple()
method works great! It returns a tuple containing the RGB components of our color objects.
We can also add class methods to our Python classes. To do this, we need to use the @classmethod
decorator as follows:
class Color:
representation = "RGB"
def __init__(self, red, green, blue):
self.red = red
self.green = green
self.blue = blue
def as_tuple(self):
return self.red, self.green, self.blue
@classmethod
def from_tuple(cls, rbg):
return cls(*rbg)
The from_tuple()
method takes a tuple object containing the RGB components of a desired color as an argument, creates a valid color object from it, and returns the object back to the caller:
>>> blue = Color.from_tuple((0, 0, 255))
>>> blue.as_tuple()
(0, 0, 255)
In this example, we use the Color
class to access the class method from_tuple()
. We can also access the method using a concrete instance of this class. However, in both cases, we'll get a completely new object.
Finally, Python classes can also have static methods that we can define with the @staticmethod
decorator:
class Color:
representation = "RGB"
def __init__(self, red, green, blue):
self.red = red
self.green = green
self.blue = blue
def as_tuple(self):
return self.red, self.green, self.blue
@classmethod
def from_tuple(cls, rbg):
return cls(*rbg)
@staticmethod
def color_says(message):
print(message)
Static methods don't operate either on the current instance self or the current class cls
. These methods can work as independent functions. However, we typically put them inside a class when they are related to the class, and we need to have them accessible from the class and its instances.
Here's how the method works:
>>> Color.color_says("Hello from the Color class!")
Hello from the Color class!
>>> red = Color(255, 0, 0)
>>> red.color_says("Hello from the red instance!")
Hello from the red instance!
This method accepts a message and prints it on your screen. It works independently from the class or instance attributes. Note that you can call the method using the class or any of its instances.
Writing Getter & Setter Methods
Programming languages like Java and C++ rely heavily on setter and getter methods to retrieve and update the attributes of a class and its instances. These methods encapsulate an attribute allowing us to get and change its value without directly accessing the attribute itself.
For example, say that we have a Label
class with a text
attribute. We can make text
a non-public attribute and provide getter and setter methods to manipulate the attributes according to our needs:
class Label:
def __init__(self, text):
self.set_text(text)
def text(self):
return self._text
def set_text(self, value):
self._text = str(value)
In this class, the text()
method is the getter associated with the ._text
attribute, while the set_text()
method is the setter for ._text
. Note how ._text
is a non-public attribute. We know this because it has a leading underscore on its name.
The setter method calls str()
to convert any input value into a string. Therefore, we can call this method with any type of object. It will convert any input argument into a string, as you will see in a moment.
If you come from programming languages like Java or C++, you need to know Python doesn't have the notion of private, protected, and public attributes. In Python, you'll use a naming convention to signal that an attribute is non-public. This convention consists of adding a leading underscore to the attribute's name. Note that this naming pattern only indicates that the attribute isn't intended to be used directly. It doesn't prevent direct access, though.
This class works as follows:
>>> label = Label("Python!")
>>> label.text()
'Python!'
>>> label.set_text("PyQt!")
>>> label.text()
'PyQt!'
>>> label.set_text(123)
>>> label.text()
'123'
In this example, we create an instance of Label
. The original text is passed to the class constructor, Label()
, which automatically calls __init__()
to set the value of ._text
by calling the setter method text()
. You can use text()
to access the label's text and set_text()
to update it. Remember that any input will be converted into a string, as we can see in the final example above.
Note that the Label
class above is just a toy example, don't confuse this class with similarly named classes from GUI frameworks like PyQt, PySide, and Tkinter.
The getter and setter pattern is pretty common in languages like Java and C++. Because PyQt and PySide are Python bindings to the Qt library, which is written in C++, you'll be using this pattern a lot in your Qt-based GUI apps. However, this pattern is less popular among Python developers. Instead, they use the @property
decorator to hide attributes behind properties.
Here's how most Python developer will write their Label
class:
class Label:
def __init__(self, text):
self.text = text
@property
def text(self):
return self._text
@text.setter
def text(self, value):
self._text = str(value)
This class defines .text
as a property. This property has getter and setter methods. Python calls them automatically when we access the attribute or update its value in an assignment:
>>> label = Label("Python!")
>>> label.text
'Python!'
>>> label.text = "PyQt"
>>> label.text
'PyQt'
>>> label.text = 123
>>> label.text
'123'
Python properties allow you to add function behavior to your attributes while permitting you to use them as normal attributes instead of as methods.
Writing Special Methods
Python supports many special methods, also known as dunder or magic methods, that are part of its class mechanism. We can identify these methods because their names start and end with a double underscore, which is the origin of their other name: dunder methods.
These methods accomplish different tasks in Python's class mechanism. They all have a common feature: Python calls them automatically depending on the operation we run.
For example, all Python objects are printable. We can print them to the screen using the print()
function. Calling print()
internally falls back to calling the target object's __str__()
special method:
>>> label = Label("Python!")
>>> print(label)
<__main__.Label object at 0x10354efd0>
In this example, we've printed our label
object. This action provides some information about the object and the memory address where it lives. However, the actual output is not very useful from the user's perspective.
Fortunately, we can improve this by providing our Label
class with an appropriate __str__()
method:
class Label:
def __init__(self, text):
self.text = text
@property
def text(self):
return self._text
@text.setter
def text(self, value):
self._text = str(value)
def __str__(self):
return self.text
The __str__()
method must return a user-friendly string representation for our objects. In this case, when we print an instance of Label
to the screen, the label's text will be displayed:
>>> label = Label("Python!")
>>> print(label)
Python!
As you can see, Python takes care of calling __str__()
automatically when we use the print()
function to display our instances of Label
.
Another special method that belongs to Python's class mechanism is __repr__()
. This method returns a developer-friendly string representation of a given object. Here, developer-friendly implies that the representation should allow a developer to recreate the object itself.
class Label:
def __init__(self, text):
self.text = text
@property
def text(self):
return self._text
@text.setter
def text(self, value):
self._text = str(value)
def __str__(self):
return self.text
def __repr__(self):
return f"{type(self).__name__}(text='{self.text}')"
The __repr__()
method returns a string representation of the current objects. This string differs from what __str__()
returns:
>>> label = Label("Python!")
>>> label
Label(text='Python!')
Now when you access the instance on your REPL session, you get a string representation of the current object. You can copy and paste this representation to recreate the object in an appropriate environment.
Reusing Code With Inheritance
Inheritance is an advanced topic in object-oriented programming. It allows you to create hierarchies of classes where each subclass inherits all the attributes and behaviors from its parent class or classes. Arguably, code reuse is the primary use case of inheritance.
Yes, we code a base class with a given functionality and make that functionality available to its subclass through inheritance. This way, we implement the functionality only once and reuse it in every subclass.
Python classes support single and multiple inheritance. For example, let's say we need to create a button class. This class needs .width
and .height
attributes that define its rectangular shape. The class also needs a label for displaying some informative text.
We can code this class from scratch, or we can use inheritance and reuse the code of our current Label
class. Here's how to do this:
class Button(Label):
def __init__(self, text, width, height):
super().__init__(text)
self.width = width
self.height = height
def __repr__(self):
return (
f"{type(self).__name__}"
f"(text='{self.text}', "
f"width={self.width}, "
f"height={self.height})"
)
To inherit from a parent class in Python, we need to list the parent class or classes in the subclass definition. To do this, we use a pair of parentheses and a comma-separated list of parent classes. If we use several parent classes, then we're using multiple inheritance, which can be challenging to reason about.
The first line in __init__()
calls the __init__()
method on the parent class to properly initialize its .text
attribute. To do this, we use the built-in super()
function. Then we define the .width
and .height
attributes, which are specific to our Button
class. Finally, we provide a custom implementation of __repr__()
.
Here's how our Button
class works:
>>> button = Button("Ok", 10, 5)
>>> button.text
'Ok'
>>> button.text = "Click Me!"
>>> button.text
'Click Me!'
>>> button.width
10
>>> button.height
5
>>> button
Button(text='Ok', width=10, height=5)
>>> print(button)
Click Me!
As you can conclude from this code, Button
has inherited the .text
attribute from Label
. This attribute is completely functional. Our class has also inherited the __str__()
method from Label
. That's why we get the button's text when we print the instance.
Using Classes in PyQt GUI Apps
Everything we've learned so far about Python classes is the basis of our future work in GUI development. When it comes to working with PyQt, PySide, Tkinter, or any other GUI framework, we'll heavily rely on our knowledge of classes and OOP because most of them are based on classes and class hierarchies.
We'll now look at how to use inheritance to create some GUI-related classes. For example, when we create an application with PyQt or PySide, we usually have a main window. To create this window, we typically inherit from QMainWindow
:
- PyQt6
- PySide6
from PyQt6.QtWidgets import QMainWindow
class Window(QMainWindow):
def __init__(self):
super().__init__()
from PySide6.QtWidgets import QMainWindow
class Window(QMainWindow):
def __init__(self):
super().__init__()
In the definition of our Window
class, we use the QMainWindow
class as the parent class. This tells Python that we want to define a class that inherits all the functionalities that QMainWindow
provides.
We can continue adding attributes and methods to our Window
class. Some of these attributes can be GUI widgets, such as labels, buttons, comboboxes, checkboxes, line edits, and many others. In PyQt, we can create all these GUI components using classes such as QLabel
, QPushButton
, QComboBox
, QCheckBox
, and QLineEdit
.
All of them have their own sets of attributes and methods that we can use according to our specific needs when designing the GUI of a given application.
Wrapping Up Classes-Related Concepts
As we've seen, Python allows us to write classes that work as templates that you can use to create concrete objects that bundle together data and behavior. The building blocks of Python classes are:
- Attributes, which hold the data in a class
- Methods, which provide the behaviors of a class
The attributes of a class define the class's data, while the methods provide the class's behaviors, which typically act on that data.
To better understand OOP and classes in Python, we should first discuss some terms that are commonly used in this aspect of Python development:
-
Classes are blueprints or templates for creating objects -- just like a blueprint for creating a car, plane, house, or anything else. In programming, this blueprint will define the data (attributes) and behavior (methods) of the object and will allow us to create multiple objects of the same kind.
-
Objects or Instances are the realizations of a class. We can create objects from the blueprint provided by the class. For example, you can create John's car from a
Car
class. -
Methods are functions defined within a class. They provide the behavior of an object of that class. For example, our
Car
class can have methods to start the engine, turn right and left, stop, and so on. -
Attributes are properties of an object or class. We can think of attributes as variables defined in a class or object. Therefore, we can have:
- class attributes, which are specific to a concrete class and common to all the instances of that class. You can access them either through the class or an object of that class. For example, if we're dealing with a single car manufacturer, then our
Car
class can have a manufacturer attribute that identifies it. - instance attributes, which are specific to a concrete instance. You can access them through the specific instance. For example, our
Car
class can have attributes to store properties such as the maximum speed, the number of passengers, the car's weight, and so on.
- class attributes, which are specific to a concrete class and common to all the instances of that class. You can access them either through the class or an object of that class. For example, if we're dealing with a single car manufacturer, then our
-
Instantiation is the process of creating an individual instance from a class. For example, we can create John's car, Jane's car, and Linda's car from our
Car
class through instantiation. In Python, this process runs through two steps:- Instance creation: Creates a new object and allocates memory for storing it.
- Instance initialization: Initializes all the attributes of the current object with appropriate values.
-
Inheritance is a mechanism of code reuse that allows us to inherit attributes and methods from one or multiple existing classes. In this context, we'll hear terms like:
- Parent class: The class we're inheriting from. This class is also known as the superclass or base class. If we have one parent class, then we're using single inheritance. If we have more than one parent class, then we're using multiple inheritance.
- Child class: The class that inherits from a given parent. This class is also known as the subclass.
Don't feel frustrated or bad if you don't understand all these terms immediately. They'll become more familiar with use as you use them in your own Python code. Many of our GUI tutorials make use of some or all of these concepts.
Conclusion
Now you know the basics of Python classes. You also learned fundamental concepts of object-oriented programming, such as inheritance. You also learned that most GUI frameworks are heavily based on classes. Therefore knowing about classes will open the door to begin building your own GUI app using PyQt, PySide, Tkinter, or any other GUI framework for Python.
PyQt6 Crash Course — a new tutorial in your Inbox every day
Beginner-focused crash course explaining the basics with hands-on examples.