Python Dunder Methods
In Python, there is a set of special methods, called ‘Dunder’ methods, which is derived from ‘double-under’, since they use double underscores as prefix and suffix, like in __init__
or __len__
. Often, these methods are also referred to as ‘magic methods’, making them look more complicated, although there is really nothing magical about them. At the end of the day – they are just another Python feature.
These so-called Dunders give us the possibility to emulate built-in behavior, in other words, to change the built-in functions to behave the way we want them to. To understand these magic methods, let’s go through some important keywords:
- Class – Category of things that have common attributes or properties
- Object – Single instance of the class, able to perform the functionalities from the class
- Self – The key to the attributes and methods in the class
__init__
- Initialization of the attributes in a class, also called a constructor in object-oriented concepts, invoked when an object is created- Overriding – Giving a new value to an attribute that already has value
Now let’s try to code something simple: class Vehicle:
class Vehicle:
# simple class called vehicle
def __init__(self, wheels=2):
# constructing the vehicle, supposing that it has at least two wheels
self.wheels = wheels
# setting the class attribute wheels to a value from the object
bicycle = Vehicle() # new object, 2 wheels by default
car = Vehicle(4) # another object, 4 wheels passed as a parameter
Explanation:
__init__
can be also referred as a constructor of the class as it is the method which runs automatically whenever a new object is created.
So far, we created a class vehicle we instantiated a parameter wheels, with a default value of 2. Then we created two objects, from which one has a parameter. If we pass an integer in the object, it will override the number of wheels, like in the object ‘car’. The __init__
method takes care of the object creation. If we try to print the ‘car’ object:
<__main__.Vehicle object at 0x7f0c884a5410>
This returns the class name, pointing that this is an object from it and its current address in memory. What if we want to print the number of wheels? We can add the function __str__
, this allows us to make a printable version of an object. Let’s see this in code:
class Vehicle:
# simple class called vehicle
def __init__(self, wheels=2):
# constructing the vehicle, supposing that it has at least two wheels
self.wheels = wheels
# setting the class attribute wheels to a value from the object
def __str__(self):
# printable object method
return "Number of wheels: {}".format(self.wheels)
# print the number of wheels
car = Vehicle(4) # new object, with a passed value
print(str(car)) # calling the overridden function str, so we can print the object
Every built-in method has a dunder method. In fact, all logical operators have a built-in method, for example: ‘+
’ has a correspondent method called __add__
, ‘-
’ has __sub__
and they can also be overloaded. You can take a look at method overloading for more details. Let’s list through some of the most important special methods.
__new__ | Called to create a new instance of a class |
__del__ | Called when an instance is about to be deleted |
__repr__ | Called to print an object, useful for debugging; used as a fallback to str |
__format__ | Called to format string literals |
__len__ | Called to return the length of an object |
__lt__ | Less then, replaces < operator |
__le__ | Less equals, replaces <= operator |
__eq__ | Equals, replaces == operator |
__ne__ | Not equals, replaces != operator |
__gt__ | Greater than, replaces > operator |
__ge__ | Greater than or equals to, replaces >= operator |
__mul__ | Multiplication, replaces * operator |
__truediv__ | Division, replaces / operators |
__getitem__ | Used to introduce value indexing, requires parameter key |
__setitem__ | Used to make items mutable, requires parameter key, items can be accessed through index |
__delitem__ | Similar to del, used to delete items through index |
__iter__ | Returns an iterator to iterate the values in a container |
Of course, there are many more dunder methods and these are just a part of the whole Python built-in method list. To get the hang of it, it requires practice. Also, you can list all the dunder methods with code. To explore we can use the __dict__
method, simply type this:
print(list.__dict__)
According to Python documentation, every object has an attribute, which is denoted in __dict__
. This dictionary method is also called mappingproxy. We will go through the __dict__
method in the following example.
Now let’s imagine that we want to create a school class that contains details about student grades. What we also want, is an easy way to count out the students. Let’s code this.
class Student:
def __init__(self, ids, name):
self.ids, self.name = ids, name
def __str__(self):
return f"Student: {self.name}, ID: {self.ids}"
# class Student, requires ID and Name of student, uses str() to return the student
class School:
def __init__(self, students, grades):
self.students, self.grades = students, grades
def __len__(self):
return len(self.students)
# class School, requires student – uses object from class Student, and grades, len() returns the number of students
students = [
Student(1, "Billy"),
Student(2, "Ann"),
Student(3, "Chris"),
] # creating object students
grades = ["C-", "B", "A+"] # creating the grades for each student
school = School(students, grades) # creating object School with the created values
print(len(school)) # returning the number of students in the school
This works, but what if we want to manage the student grades? So far, students are not accessible, let’s try __setitem__
and __getitem__
.
class Student:
def __init__(self, ids, name):
self.ids, self.name = ids, name
def __str__(self):
return f"Student: {self.name}, ID: {self.ids}"
class School:
def __init__(self, student, grades):
self.student, self.grades = student, grades
def __len__(self):
return len(self.student)
# ADDED: Change the grade of a certain student
def __setitem__(self, key, value):
self.grades[key] = value
# ADDED: Return the grade from a certain student
def __getitem__(self, key):
return self.student[key].name, self.grades[key]
students = [Student(1, "Billy"), Student(2, "Ann"), Student(3, "Chris")]
grades = ["C-", "B", "A+"]
school = School(students, grades)
print(len(school))
print(school[0]) # Prints the 0th student, name and grade
school[0] = "B" # Billy got a ‘B’ now!
print(school[0]) # Prints the 0th student, name and grade again, only updated.
Output
3
('Billy', 'C-')
('Billy', 'B')
Now, let’s call the __dict__
method from the class names.
print(School.__dict__)
print(Student.__dict__)
{'__module__': '__main__',
'__init__': <function School.__init__ at 0x7fe920594560>,
'__len__': <function School.__len__ at 0x7fe9205945f0>,
'__setitem__': <function School.__setitem__ at 0x7fe920594680>,
'__getitem__': <function School.__getitem__ at 0x7fe920594710>,
'__dict__': <attribute '__dict__' of 'School' objects>,
'__weakref__': <attribute '__weakref__' of 'School' objects>,
'__doc__': None}
{'__module__': '__main__', '__init__': <function Student.__init__ at 0x7fe920594440>, '__str__': <function Student.__str__ at 0x7fe9205944d0>, '__dict__': <attribute '__dict__' of 'Student' objects>, '__weakref__': <attribute '__weakref__' of 'Student' objects>, '__doc__': None}
This method allows us to observe the functionalities built in the classes. It is also possible to call the dict method to access specific functions, simply by typing:
print(School.__dict__['__init__'])
and the output is:
<function School.__init__ at 0x7f4bc1b8b560>
This is another useful feature in Python that allows users to analyze huge classes easily. Having all these ‘tricks up the sleeve’ is exceptionally important when a problem occurs. To master the dunder methods, you need to implement code on your own. Certain things can only be learned through typing code. These magic methods are extremely useful to make a class interactive. They make the class compatible with the built-in methods. Another bonus is that you don’t need to remember the name of the function, you just remember the built-in methods. This way, Python is consistent with its syntax.