Image by Author
In Python, magic methods help you emulate the behavior of built-in functions in your Python classes. These methods have leading and trailing double underscores (__), and hence are also called dunder methods.
These magic methods also help you implement operator overloading in Python. You’ve probably seen examples of this. Like using the multiplication operator * with two integers gives the product. While using it with a string and an integer k
gives the string repeated k
times:
>>> 3 * 4
12
>>> 'code' * 3
'codecodecode'
In this article, we’ll explore magic methods in Python by creating a simple two-dimensional vector Vector2D
class.
We’ll start with methods you’re likely familiar with and gradually build up to more helpful magic methods.
Let’s start writing some magic methods!
Consider the following Vector2D
class:
Once you create a class and instantiate an object, you can add attributes like so: obj_name.attribute_name = value
.
However, instead of manually adding attributes to every instance that you create (not interesting at all, of course!), you need a way to initialize these attributes when you instantiate an object.
To do so you can define the __init__
method. Let’s define the define the __init__
method for our Vector2D
class:
class Vector2D:
def __init__(self, x, y):
self.x = x
self.y = y
v = Vector2D(3, 5)
When you try to inspect or print out the object you instantiated, you’ll see that you don’t get any helpful information.
v = Vector2D(3, 5)
print(v)
Output >>> <__main__.Vector2D object at 0x7d2fcfaf0ac0>
This is why you should add a representation string, a string representation of the object. To do so, add a __repr__
method like so:
class Vector2D:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Vector2D(x={self.x}, y={self.y})"
v = Vector2D(3, 5)
print(v)
Output >>> Vector2D(x=3, y=5)
The __repr__
should include all the attributes and information needed to create an instance of the class. The __repr__
method is typically used for the purpose of debugging.
The __str__
is also used to add a string representation of the object. In general, the __str__
method is used to provide info to the end users of the class.
Let’s add a __str__
method to our class:
class Vector2D:
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
return f"Vector2D(x={self.x}, y={self.y})"
v = Vector2D(3, 5)
print(v)
Output >>> Vector2D(x=3, y=5)
If there is no implementation of __str__
, it falls back to __repr__
. So for every class that you create, you should—at the minimum—add a __repr__
method.
Next, let’s add a method to check for equality of any two objects of the Vector2D
class. Two vector objects are equal if they have identical x and y coordinates.
Now create two Vector2D
objects with equal values for both x and y and compare them for equality:
v1 = Vector2D(3, 5)
v2 = Vector2D(3, 5)
print(v1 == v2)
The result is False. Because by default the comparison checks for equality of the object IDs in memory.
Let’s add the __eq__
method to check for equality:
class Vector2D:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Vector2D(x={self.x}, y={self.y})"
def __eq__(self, other):
return self.x == other.x and self.y == other.y
The equality checks should now work as expected:
v1 = Vector2D(3, 5)
v2 = Vector2D(3, 5)
print(v1 == v2)
Python’s built-in len()
function helps you compute the length of built-in iterables. Let’s say, for a vector, length should return the number of elements that the vector contains.
So let’s add a __len__
method for the Vector2D
class:
class Vector2D:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Vector2D(x={self.x}, y={self.y})"
def __len__(self):
return 2
v = Vector2D(3, 5)
print(len(v))
All objects of the Vector2D
class are of length 2:
Now let’s think of common operations we’d perform on vectors. Let’s add magic methods to add and subtract any two vectors.
If you directly try to add two vector objects, you’ll run into errors. So you should add an __add__
method:
class Vector2D:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Vector2D(x={self.x}, y={self.y})"
def __add__(self, other):
return Vector2D(self.x + other.x, self.y + other.y)
You can now add any two vectors like so:
v1 = Vector2D(3, 5)
v2 = Vector2D(1, 2)
result = v1 + v2
print(result)
Output >>> Vector2D(x=4, y=7)
Next, let’s add a __sub__
method to calculate the difference between any two objects of the Vector2D
class:
class Vector2D:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Vector2D(x={self.x}, y={self.y})"
def __sub__(self, other):
return Vector2D(self.x - other.x, self.y - other.y)
v1 = Vector2D(3, 5)
v2 = Vector2D(1, 2)
result = v1 - v2
print(result)
Output >>> Vector2D(x=2, y=3)
We can also define a __mul__
method to define multiplication between objects.
Let’s implement let’s handle
- Scalar multiplication: the multiplication of a vector by scalar and
- Inner product: the dot product of two vectors
class Vector2D:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Vector2D(x={self.x}, y={self.y})"
def __mul__(self, other):
# Scalar multiplication
if isinstance(other, (int, float)):
return Vector2D(self.x * other, self.y * other)
# Dot product
elif isinstance(other, Vector2D):
return self.x * other.x + self.y * other.y
else:
raise TypeError("Unsupported operand type for *")
Now we’ll take a couple of examples to see the __mul__
method in action.
v1 = Vector2D(3, 5)
v2 = Vector2D(1, 2)
# Scalar multiplication
result1 = v1 * 2
print(result1)
# Dot product
result2 = v1 * v2
print(result2)
Output >>>
Vector2D(x=6, y=10)
13
The __getitem__
magic method allows you to index into the objects and access attributes or slice of attributes using the familiar square-bracket [ ] syntax.
For an object v
of the Vector2D
class:
v[0]
: x coordinatev[1]
: y coordinate
If you try accessing by index, you’ll run into errors:
v = Vector2D(3, 5)
print(v[0],v[1])
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
in ()
----> 1 print(v[0],v[1])
TypeError: 'Vector2D' object is not subscriptable |
Let’s implement the __getitem__
method:
class Vector2D:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Vector2D(x={self.x}, y={self.y})"
def __getitem__(self, key):
if key == 0:
return self.x
elif key == 1:
return self.y
else:
raise IndexError("Index out of range")
You can now access the elements using their indexes as shown:
v = Vector2D(3, 5)
print(v[0])
print(v[1])
With an implementation of the __call__
method, you can call objects as if they were functions.
In the Vector2D
class, we can implement a __call__
to scale a vector by a given factor:
class Vector2D:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Vector2D(x={self.x}, y={self.y})"
def __call__(self, scalar):
return Vector2D(self.x * scalar, self.y * scalar)
So if you now call 3, you’ll get the vector scaled by factor of 3:
v = Vector2D(3, 5)
result = v(3)
print(result)
Output >>> Vector2D(x=9, y=15)
The __getattr__
method is used to get the values of specific attributes of the objects.
For this example, we can add a __getattr__
dunder method that gets called to compute the magnitude (L2-norm) of the vector:
class Vector2D:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Vector2D(x={self.x}, y={self.y})"
def __getattr__(self, name):
if name == "magnitude":
return (self.x ** 2 + self.y ** 2) ** 0.5
else:
raise AttributeError(f"'Vector2D' object has no attribute '{name}'")
Let’s verify if this works as expected:
v = Vector2D(3, 4)
print(v.magnitude)
That’s all for this tutorial! I hope you learned how to add magic methods to your class to emulate the behavior of built-in functions.
We’ve covered some of the most useful magic methods. But this is not this is not an exhaustive list. To further your understanding, create a Python class of your choice and add magic methods depending on the functionality required. Keep coding!
Bala Priya C is a developer and technical writer from India. She likes working at the intersection of math, programming, data science, and content creation. Her areas of interest and expertise include DevOps, data science, and natural language processing. She enjoys reading, writing, coding, and coffee! Currently, she’s working on learning and sharing her knowledge with the developer community by authoring tutorials, how-to guides, opinion pieces, and more.