Understanding special methods in Python

Python

21 Jul 2020 | 17 minute read

Table of Contents

When diving into the world of Python, you might have encountered methods such as the __init__, __str__ or __getitem__ methods. These are special methods that are called by the Python interpreter and not by you, and this article will showcase how they are used and the power of them.

As there are numerous special methods across the Python programing language, this article won't explore all of them. Instead, we'll focus on a few, and as your understanding deepens on how they are used you'll be able to extrapolate how others are used.

By understanding how special methods work, you're on your way to understanding the Python data model in more detail, and what makes Python code pythonic. Ultimately, it will enable you to become a more experienced Python developer.

Special, magic and dunder methods

Special methods are known by many names, the most common ones being magic methods and dunder methods. These are just different names for the same thing, and nothing you need to worry about.

Why use special methods?

Special methods are called by the Python interpreter and not by the user. As of this, special methods creates somewhat of a contract between Python developers, as there is a unified way to reach the same result.

As of this, you can avoid counter-intuitive implementations and write code in a more standardized way. This enables you to write code not only more pythonic, but code that is easier to understand and grasp for other Python developers. Ultimately, by defining special methods in your classes, your classes will start to resemble built-in types.

Furthermore, some functions in the Python standard library might expect you to implement one or more special methods to use the function on that object.

Your first special methods

When exploring special methods and how they are used, we will build on an example throughout the entire article. In our example, we are to simulate student attendance for a class, where the students have been hard-coded in the private variable _students with their names sorted in alphabetical order.

By building upon a real example, and not have separate examples for each special method, I hope that you will get a better understanding of how to utilise special methods in your classes.

What am I? (__init__)

When creating an instance of the ClassAttendants class, we want to initialise the object with the _students. To do so, we utilise the __init__ special method, which is the constructor for a class in Python.

The __init__ special method will run when an object is created (attendants = ClassAttendants()), and is typically where variables are defined on the object.

As you can see in the snippet below, we assign the _students variable to the object itself, where self represents the instance of the object itself.

class ClassAttendants:
    def __init__(self):
        self._students = sorted(
            [
                "Odin",
                "Frigg",
                "Balder",
                "Loki",
                "Thor",
                "Freya",
                "Frey",
                "Heimdall",
                "Hel",
                "Vidar",
                "Vale",
                "Harbard",
                "Kvasir",
                "Njord",
            ]
        )

Having defined our __init__ special method, the _students variable will be defined on our objects when they are created.

How many students attended the class? (__len__)

To evaluate how many students attended the class, we need to know the length of the _students list. To do so, we could do the following:

attendants = ClassAttendants()
len(attendants._students)

Which would give the following output:

14

As you can see, this approach works fine and produces the desired result. However, it as not encouraged as _students is a private variable. Furthermore, there are numerous benefits to using special methods as mentioned earlier in this article.

A more Pythonic approach would be to implement the __len__ method on the ClassAttendants class. By doing so the implementation details of the ClassAttendants class, such as the variable naming (_students), would be hidden for any developer using the class. This is desirable, as it enables any developer to use your class without knowing the exact implementation of it.

Your code would also subscribe to the common practice of using the len function to get the length of the object you're working with.

class ClassAttendants:
    def __init__(self):
        self._students = sorted(
            [
                "Odin",
                "Frigg",
                "Balder",
                "Loki",
                "Thor",
                "Freya",
                "Frey",
                "Heimdall",
                "Hel",
                "Vidar",
                "Vale",
                "Harbard",
                "Kvasir",
                "Njord",
            ]
        )

    def __len__(self):
        return len(self._students)

To execute the __len__ method, you shouldn't call it directly. As mentioned earlier, special methods are called by the Python interpreter and not by the developer.

Instead, you should create a ClassAttendants instance, and call the len function with the ClassAttendants object as the argument. See the snippet below.

attendants = ClassAttendants()
len(attendants)

By doing so, the interpreter will deduct that you want to execute the __len__ method and do so for you. The snippet above produces the following output:

14

Which students attended the class? (__str__)

When calling print on a ClassAttendants object, we want to display the students attending the class in a format that is readable for the (human) caller of the function.

What happens if we try to print a ClassAttendants objects?

attendants = ClassAttendants()
print(attendants)
<classroom.ClassAttendants object at 0x7f7802d38860>

As you can see, this isn't quite readable for humans but gives us some context on what kind of object it is. It would be desirable to know the names of the attendants.

To display a string readable for humans, we need to implement the special method __str__ on ClassAttendants. The __str__ method is called when a string readable representation is requested, such as when the print function is called.

class ClassAttendants:
    def __init__(self):
        self._students = sorted(
            [
                "Odin",
                "Frigg",
                "Balder",
                "Loki",
                "Thor",
                "Freya",
                "Frey",
                "Heimdall",
                "Hel",
                "Vidar",
                "Vale",
                "Harbard",
                "Kvasir",
                "Njord",
            ]
        )

    def __len__(self):
        return len(self._students)

    def __str__(self):
        return ", ".join(self._students)

To generate a readable string for the attending students of the class, we return the names of all students separated by a comma in the __str__ method.

attendants = ClassAttendants()
print(attendants)
Balder, Frey, Freya, Frigg, Harbard, Heimdall, Hel, Kvasir, Loki, Njord, Odin, Thor, Vale, Vidar

By implementing the __str__ method, we're now able to print our ClassAttendants objects in a human-readable manner.

Get attendant(s) (__getitem__)

Getting an attending student via the index in the _students_ list can be achieved using the special method __getitem__. This allows us to use the [] syntax on a ClassAttendants object.

Just as with the len() example, the list of students could be accessed directly, but as previously mentioned it's favourable to implement the special method. Furthermore, _students is declared as a private variable and should therefore not be accessed directly.

As you can see in the code below, the __getitem__ method accepts a position argument and returns the self._students element at the corresponding position.

class ClassAttendants:
    def __init__(self):
        self._students = sorted(
            [
                "Odin",
                "Frigg",
                "Balder",
                "Loki",
                "Thor",
                "Freya",
                "Frey",
                "Heimdall",
                "Hel",
                "Vidar",
                "Vale",
                "Harbard",
                "Kvasir",
                "Njord",
            ]
        )

    def __len__(self):
        return len(self._students)

    def __str__(self):
        return ", ".join(self._students)

    def __getitem__(self, position):
        return self._students[position]

After implementing the __getitem__ method, the [] operator can be used on any ClassAttendants object.

attendants = ClassAttendants()
attendants[0]
'Balder'

As __getitem__ passes the [] operator to the _students list a slice is a valid position argument for __getitem__ as the list data structure supports slices.

This is useful when fetching more than multiple objects. For example, we might get the first five attending students.

attendants = ClassAttendants()
attendants[0:5]
['Balder', 'Frey', 'Freya', 'Frigg', 'Harbard']

Or we might get all students attending the class.

attendants = ClassAttendants()
attendants[:]
['Balder', 'Frey', 'Freya', 'Frigg', 'Harbard', 'Heimdall', 'Hel', 'Kvasir', 'Loki', 'Njord', 'Odin', 'Thor', 'Vale', 'Vidar']

Update attendant (__setitem__)

Sometimes, we need to update the object. For example, we might want to replace Freya with Freja (the Swedish spelling of Freya) on the 2nd position of the list of students who attended the class.

Let's try to do so.

attendants = ClassAttendants()
attendants[2] = "Freja"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'ClassAttendants' object does not support item assignment

Unfortunately, that didn't work. To update the _students variable, we need to implement the special method __setitem__, which closely resembles the __getitem__ method.

The difference between these two methods is that the __setitem__ method also accepts the value parameter, which is the value to be assigned at the given position.

By inspecting the two methods in the implementation below, pay close attention to their differences and I'm sure you'll find it quite easy to grasp.

class ClassAttendants:
    def __init__(self):
        self._students = sorted(
            [
                "Odin",
                "Frigg",
                "Balder",
                "Loki",
                "Thor",
                "Freya",
                "Frey",
                "Heimdall",
                "Hel",
                "Vidar",
                "Vale",
                "Harbard",
                "Kvasir",
                "Njord",
            ]
        )

    def __len__(self):
        return len(self._students)

    def __str__(self):
        return ", ".join(self._students)

    def __getitem__(self, position):
        return self._students[position]

    def __setitem__(self, position, value):
        self._students[position] = value

Now, let's try to update the 2nd position again.

attendants = ClassAttendants()
attendants[2] = "Freja"
print(attendants)
Balder, Frey, Freja, Frigg, Harbard, Heimdall, Hel, Kvasir, Loki, Njord, Odin, Thor, Vale, Vidar

As you can see, the attending student at position 2 has been updated from Freya to Freja.

Conclusion

In this article, we've touched on how special methods can enable you to write code that is more pythonic, hides implementation details, and enables users of your class to do more with less code.

I invite you to further explore how the use of special methods can enable you to write cleaner and better code, and improve your Python skills.