Now Reading
Comprehensive Guide To Python Dunder Methods

Comprehensive Guide To Python Dunder Methods

Aditya Singh
dunder-methods

Python has a set of magic methods that can be used to enrich data classes; they are special in the way they are invoked. These methods are also called “dunder methods” because they start and end with double underscores. Dunder methods allow developers to emulate built-in methods, and it’s also how operator overloading is implemented in Python. For example, when we add two integers together, 4 + 2, and when we add two strings together, “machine” + “learning”, the behaviour is different. The strings get concatenated while the integers are actually added together. 

The “Essential” Dunder Methods

If you have ever created a class of your own, you already know one of the dunder methods, __init__(). Although it’s often referred to as the constructor, it’s not the real constructor; the __new__() method is the constructor. The superclass’s  __new__() , super().__new__(cls[, ...]), method is invoked, which creates an instance of the class, which is then passed to the __init__() along with other arguments. Why go through the ordeal of creating the __new__() method? You don’t need to; the __new__() method was created mainly to facilitate the creation of subclasses of immutable types (such as int, str, list) and metaclasses. 

 class Vector():
     def __new__(cls, x, y):
         print("__new__ was invoked")
         instance = object.__new__(cls)
         return instance
     def __init__(self, x, y):
         print("__init__ was invoked")
         self.x = x
         self.y = y
  vector1 = Vector(12, 8)

-----------------------------Output-----------------------------
  __new__ was invoked
  __init__ was invoked 

In addition to __init__()  there are two dunder methods that you should always implement: __repr__() and __str__()

__repr__() defines the “official” string representation of the object. Ideally, it should output a string that is a valid Python statement and can be used to recreate the object. It is mainly used for debugging. 

def __repr__(self):
    return f"Vector({self.x}, {self.y})" 

The __str__() method also return a string representation of the object; however, this representation doesn’t need to be a valid Python statement. It is used by built-in functions like format() and print(), so the string representation should be readable for the end-user. If __str__() method is not defined it invokes the __repr__() method.

def __str__(self):
    return f"{self.x}x + {self.y}y"
 # before implementing __str__
 print(vector1) #or print(repr(vector1)) 
 # __str__ implemented
 print(vector1)

-----------------------------Output-----------------------------
 Vector(12, 8)
 12x + 8y

Emulating Built-in Functions

__len__()  is used to implement the built-in len() method. It should return the length of the object. For the vector example, it makes sense if len() returns the magnitude of the vector, but the return type of len() is restricted to integers.

def __len__(self):
     return int((self.x*self.x +self.y*self.y)**(1/2)) 

The __getitem__() and __setitem__() methods are used to implement the built-in functionality of using [index/key] to read and edit elements of a sequence object like a list or a mapping object like dict. For the vector class example, you can use the object[index] to read and edit the variables instead of creating individual getter and setter methods for both instance variables.

def __getitem__(self, key):
     if key < 0 or key > 1:
         raise IndexError("Index out of range! Should either be 0 or 1.")
     elif key:
         return self.y
     else:
         return self.x
 
def __setitem__(self, key, val):
     if key < 0 or key > 1:
         raise IndexError("Index out of range! Should either be 0 or 1.")
     elif key:
         self.y = val
     else:
         self.x = val 

Like functions, Python objects are callable, the __call__() method defines what happens when an object is called. You can use this to overcome the type restriction of the len() method and return the magnitude of the vector in float.

 def __call__(self):
     print(f"Vector({self.x}, {self.y}) was called.")
     return (self.x*self.x +self.y*self.y)**(1/2) 
 # invoke __len__
 print(len(vector1))
 # use [] to edit x component of the vector
 vector1[0] = 9
 print(vector1)
 # call the Vector object
 mod = vector1()
 print(mod)

-----------------------------Output-----------------------------
 12
 9x + 8y
 Vector(9, 8) was called.
 12.041594578792296 

Operator Overriding Using Dunder Methods

Dunder methods like __add__(self, other), __sub__(self, other), __mul__(self, other), __mod__(self, other), etc are used to implement binary arithmetic operations. Let’s say you’re interested in supporting the addition, subtraction and multiplication(dot) operations on two vectors:

def __add__(self, other):
    if type(other) is not Vector:
        raise TypeError('other should be an object of class Vector')
    return Vector(self.x + other.x, self.y + other.y)
 
def __sub__(self, other):
    if type(other) is not Vector:
        raise TypeError('other should be an object of class Vector')
    return Vector(self.x - other.x, self.y - other.y)
 
def __mul__(self, other):
    if type(other) is not Vector:
        raise TypeError('other should be an object of class Vector')
    return self.x * other.x + self.y * other.y 
 vector2 = Vector(8, -1)
 print(vector1 + vector2)
 print(vector1 - vector2)
 print(vector1 * vector2)

-----------------------------Output-----------------------------
 17x + 7y
 1x + 9y
 64 

Python also has a set of “rich comparison” dunder methods that are used to override the behaviour of conditional operators such as <, >, ==, <=, etc. Continuing the vector example, let’s say you want to support the less than, greater than and equal to operators on a pair of vectors: 

def __lt__(self, other):
     if type(other) is not Vector:
         raise TypeError('other should be an object of class Vector')
     return self() < other()
     
def __gt__(self, other):
     if type(other) is not Vector:
         raise TypeError('other should be an object of class Vector')
     return self() > other()
     
def __eq__(self, other):
     if type(other) is not Vector:
         raise TypeError('other should be an object of class Vector')
     return self.x == other.x and self.y == other.y
 print(vector1 > vector2)
 print(vector3 < vector2)
 print(vector1 == vector3)

-----------------------------Output-----------------------------
 True
 False
 True 

The consolidated Vector class can be found in a gist here.

Last Epoch

This article discussed Python dunder methods. Although it didn’t go through all of the dunder methods Python offers, the ones discussed should be enough to enable you to write better,  more sophisticated classes in Python and ease the process of understanding (the source code of) built-in and third-party modules. To learn more about dunder methods and the Python data model, refer to the official documentation

Want to learn more about the ins and outs of Python? Check out these articles:

What Do You Think?

Join Our Telegram Group. Be part of an engaging online community. Join Here.

Subscribe to our Newsletter

Get the latest updates and relevant offers by sharing your email.

Copyright Analytics India Magazine Pvt Ltd

Scroll To Top