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:

<code> >>> 3 * 4 12 >>> 'code' * 3 'codecodecode'</code>

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:

<code>class Vector2D: def __init__(self, x, y): self.x = x self.y = y v = Vector2D(3, 5)</code>

When you try to inspect or print out the object you instantiated, you’ll see that you don’t get any helpful information.

<code>v = Vector2D(3, 5) print(v)</code>

<code>Output >>> <__main__.Vector2D object at 0x7d2fcfaf0ac0></code>

This is why you should add a representation string, a string representation of the object. To do so, add a `__repr__`

method like so:

<code>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)</code>

<code>Output >>> Vector2D(x=3, y=5)</code>

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:

<code>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)</code>

<code>Output >>> Vector2D(x=3, y=5)</code>

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:

<code>v1 = Vector2D(3, 5) v2 = Vector2D(3, 5) print(v1 == v2)</code>

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:

<code>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</code>

The equality checks should now work as expected:

<code>v1 = Vector2D(3, 5) v2 = Vector2D(3, 5) print(v1 == v2)</code>

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:

<code>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))</code>

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:

<code>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)</code>

You can now add any two vectors like so:

<code>v1 = Vector2D(3, 5) v2 = Vector2D(1, 2) result = v1 + v2 print(result)</code>

<code>Output >>> Vector2D(x=4, y=7)</code>

Next, let’s add a `__sub__`

method to calculate the difference between any two objects of the `Vector2D`

class:

<code>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)</code>

<code>v1 = Vector2D(3, 5) v2 = Vector2D(1, 2) result = v1 - v2 print(result)</code>

<code>Output >>> Vector2D(x=2, y=3)</code>

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

<code>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 *")</code>

Now we’ll take a couple of examples to see the `__mul__`

method in action.

<code>v1 = Vector2D(3, 5) v2 = Vector2D(1, 2) # Scalar multiplication result1 = v1 * 2 print(result1) # Dot product result2 = v1 * v2 print(result2)</code>

<code>Output >>> Vector2D(x=6, y=10) 13</code>

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 coordinate`v[1]`

: y coordinate

If you try accessing by index, you’ll run into errors:

<code>v = Vector2D(3, 5) print(v[0],v[1])</code>

<code>--------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-6-3fbbbf13d881> in <cell line:="">() ----> 1 print(v[0],v[1]) TypeError: 'Vector2D' object is not subscriptable</cell></ipython-input-6-3fbbbf13d881></code>

Let’s implement the `__getitem__`

method:

<code>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")</code>

You can now access the elements using their indexes as shown:

<code>v = Vector2D(3, 5) print(v[0]) print(v[1])</code>

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:

<code>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)</code>

So if you now call 3, you’ll get the vector scaled by factor of 3:

<code>v = Vector2D(3, 5) result = v(3) print(result)</code>

<code>Output >>> Vector2D(x=9, y=15)</code>

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:

<code>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}'")</code>

Let’s verify if this works as expected:

<code>v = Vector2D(3, 4) print(v.magnitude)</code>

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.