Operator and Function Overloading in Python

Operator and Function Overloading in Custom Python Classes

by Malay Agarwal May 08, 2018 intermediate python

If you’ve used the + or * operator on a str object in Python, you must have noticed its different behavior when compared to int or float objects:

>>>
>>> # Adds the two numbers
>>> 1 + 2
3

>>> # Concatenates the two strings
>>> 'Real' + 'Python'
'RealPython'


>>> # Gives the product
>>> 3 * 2
6

>>> # Repeats the string
>>> 'Python' * 3
'PythonPythonPython'

You might have wondered how the same built-in operator or function shows different behavior for objects of different classes. This is called operator overloading or function overloading respectively. This article will help you understand this mechanism, so that you can do the same in your own Python classes and make your objects more Pythonic.

You’ll learn the following:

  • The API that handles operators and built-ins in Python
  • The “secret” behind len() and other built-ins
  • How to make your classes capable of using operators
  • How to make your classes compatible with Python’s built-in functions

As a bonus, you’ll also see an example class, objects of which will be compatible with many of these operators and functions. Let’s get started!

The Python Data Model

Say you have a class representing an online order having a cart (a list) and a customer (a str or instance of another class which represents a customer).

In such a case, it is quite natural to want to obtain the length of the cart list. Someone new to Python might decide to implement a method called get_cart_len() in their class to do this. But you can configure the built-in len() in such a way that it returns the length of the cart list when given our object.

In another case, we might want to append something to the cart. Again, someone new to Python would think of implementing a method called append_to_cart() that takes an item and appends it to the cart list. But you can configure the + operator in such a way that it appends a new item to the cart.

Python does all this using special methods. These special methods have a naming convention, where the name starts with two underscores, followed by an identifier and ends with another pair of underscores.

Essentially, each built-in function or operator has a special method corresponding to it. For example, there’s __len__(), corresponding to len(), and __add__(), corresponding to the + operator.

By default, most of the built-ins and operators will not work with objects of your classes. You must add the corresponding special methods in your class definition to make your object compatible with built-ins and operators.

When you do this, the behavior of the function or operator associated with it changes according to that defined in the method.

This is exactly what the Data Model (Section 3 of the Python documentation) helps you accomplish. It lists all the special methods available and provides you with the means of overloading built-in functions and operators so that you can use them on your own objects.

Let’s see what this means.

The Internals of Operations Like len() and []

Every class in Python defines its own behavior for built-in functions and methods. When you pass an instance of some class to a built-in function or use an operator on the instance, it is actually equivalent to calling a special method with relevant arguments.

If there is a built-in function, func(), and the corresponding special method for the function is __func__(), Python interprets a call to the function as obj.__func__(), where obj is the object. In the case of operators, if you have an operator opr and the corresponding special method for it is __opr__(), Python interprets something like obj1 <opr> obj2 as obj1.__opr__(obj2).

So, when you’re calling len() on an object, Python handles the call as obj.__len__(). When you use the [] operator on an iterable to obtain the value at an index, Python handles it as itr.__getitem__(index), where itr is the iterable object and index is the index you want to obtain.

Therefore, when you define these special methods in your own class, you override the behavior of the function or operator associated with them because, behind the scenes, Python is calling your method. Let’s get a better understanding of this:

>>>
>>> a = 'Real Python'
>>> b = ['Real', 'Python']
>>> len(a)
11
>>> a.__len__()
11
>>> b[0]
'Real'
>>> b.__getitem__(0)
'Real'

As you can see, when you use the function or its corresponding special method, you get the same result. In fact, when you obtain the list of attributes and methods of a str object using dir(), you’ll see these special methods in the list in addition to the usual methods available on str objects:

>>>
>>> dir(a)
['__add__',
 '__class__',
 '__contains__',
 '__delattr__',
 '__dir__',
 ...,
 '__iter__',
 '__le__',
 '__len__',
 '__lt__',
 ...,
 'swapcase',
 'title',
 'translate',
 'upper',
 'zfill']

If the behavior of a built-in function or operator is not defined in the class by the special method, then you will get a TypeError.

So, how can you use special methods in your classes?

Overloading Built-in Functions

Many of the special methods defined in the Data Model can be used to change the behavior of functions such as len, abs, hash, divmod, and so on. To do this, you only need to define the corresponding special method in your class. Let’s look at a few examples:

Giving a Length to Your Objects Using len()

To change the behavior of len(), you need to define the __len__() special method in your class. Whenever you pass an object of your class to len(), your custom definition of __len__() will be used to obtain the result. Let’s implement len() for the order class we talked about in the beginning:

>>>
>>> class Order:
...     def __init__(self, cart, customer):
...         self.cart = list(cart)
...         self.customer = customer
...
...     def __len__(self):
...         return len(self.cart)
...
>>> order = Order(['banana', 'apple', 'mango'], 'Real Python')
>>> len(order)
3

As you can see, you can now use len() to directly obtain the length of the cart. Moreover, it makes more intuitive sense to say “length of order” rather than calling something like order.get_cart_len(). Your call is both Pythonic and more intuitive. When you don’t have the __len__() method defined but still call len() on your object, you get a TypeError:

>>>
>>> class Order:
...     def __init__(self, cart, customer):
...         self.cart = list(cart)
...         self.customer = customer
...
>>> order = Order(['banana', 'apple', 'mango'], 'Real Python')
>>> len(order)  # Calling len when no __len__
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: object of type 'Order' has no len()

But, when overloading len(), you should keep in mind that Python requires the function to return an integer. If your method were to return anything other than an integer, you would get a TypeError. This, most probably, is to keep it consistent with the fact that len() is generally used to obtain the length of a sequence, which can only be an integer:

>>>
>>> class Order:
...     def __init__(self, cart, customer):
...         self.cart = list(cart)
...         self.customer = customer
...
...     def __len__(self):
...         return float(len(self.cart))  # Return type changed to float
...
>>> order = Order(['banana', 'apple', 'mango'], 'Real Python')
>>> len(order)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'float' object cannot be interpreted as an integer

Making Your Objects Work With abs()

You can dictate the behavior of the abs() built-in for instances of your class by defining the __abs__() special method in the class. There are no restrictions on the return value of abs(), and you get a TypeError when the special method is absent in your class definition.

In a class representing a vector in a two-dimensional space, abs() can be used to get the length of the vector. Let’s see it in action:

>>>
>>> class Vector:
...     def __init__(self, x_comp, y_comp):
...         self.x_comp = x_comp
...         self.y_comp = y_comp
...
...     def __abs__(self):
...         return (x * x + y * y) ** 0.5
...
>>> vector = Vector(3, 4)
>>> abs(vector)
5.0

It makes more intuitive sense to say “absolute value of vector” rather than calling something like vector.get_mag().

Printing Your Objects Prettily Using str()

The str() built-in is used to cast an instance of a class to a str object, or more appropriately, to obtain a user-friendly string representation of the object which can be read by a normal user rather than the programmer. You can define the string format your object should be displayed in when passed to str() by defining the __str__() method in your class. Moreover, __str__() is the method that is used by Python when you call print() on your object.

Let’s implement this in the Vector class to format Vector objects as xi+yj. A negative y-component will be handled using the format mini-language:

>>>
>>> class Vector:
...     def __init__(self, x_comp, y_comp):
...         self.x_comp = x_comp
...         self.y_comp = y_comp
...
...     def __str__(self):
...         # By default, sign of +ve number is not displayed
...         # Using `+`, sign is always displayed
...         return f'{self.x_comp}i{self.y_comp:+}j'
...
>>> vector = Vector(3, 4)
>>> str(vector)
'3i+4j'
>>> print(vector)
3i+4j

It is necessary that __str__() returns a str object, and we get a TypeError if the return type is non-string.

Representing Your Objects Using repr()

The repr() built-in is used to obtain the parsable string representation of an object. If an object is parsable, that means that Python should be able to recreate the object from the representation when repr is used in conjunction with functions like eval(). To define the behavior of repr(), you can use the __repr__() special method.

This is also the method Python uses to display the object in a REPL session. If the __repr__() method is not defined, you will get something like <__main__.Vector object at 0x...> trying to look at the object in the REPL session. Let’s see it in action in the Vector class:

>>>
>>> class Vector:
...     def __init__(self, x_comp, y_comp):
...         self.x_comp = x_comp
...         self.y_comp = y_comp
...
...     def __repr__(self):
...         return f'Vector({self.x_comp}, {self.y_comp})'
...

>>> vector = Vector(3, 4)
>>> repr(vector)
'Vector(3, 4)'

>>> b = eval(repr(vector))
>>> type(b), b.x_comp, b.y_comp
(__main__.Vector, 3, 4)

>>> vector  # Looking at object; __repr__ used
'Vector(3, 4)'

Making Your Objects Truthy or Falsey Using bool()

The bool() built-in can be used to obtain the truth value of an object. To define its behavior, you can use the __bool__() (__nonzero__() in Python 2.x) special method.

The behavior defined here will determine the truth value of an instance in all contexts that require obtaining a truth value such as in if statements.

As an example, for the Order class that was defined above, an instance can be considered to be truthy if the length of the cart list is non-zero. This can be used to check whether an order should be processed or not:

>>>
>>> class Order:
...     def __init__(self, cart, customer):
...         self.cart = list(cart)
...         self.customer = customer
...
...     def __bool__(self):
...         return len(self.cart) > 0
...
>>> order1 = Order(['banana', 'apple', 'mango'], 'Real Python')
>>> order2 = Order([], 'Python')

>>> bool(order1)
True
>>> bool(order2)
False

>>> for order in [order1, order2]:
...     if order:
...         print(f"{order.customer}'s order is processing...")
...     else:
...         print(f"Empty order for customer {order.customer}")
Real Python's order is processing...
Empty order for customer Python

There are many more special methods that overload built-in functions. You can find them in the documentation. Having discussed some of them, let’s move to operators.

Overloading Built-in Operators

Changing the behavior of operators is just as simple as changing the behavior of functions. You define their corresponding special methods in your class, and the operators work according to the behavior defined in these methods.

These are different from the above special methods in the sense that they need to accept another argument in the definition other than self, generally referred to by the name other. Let’s look at a few examples.

Making Your Objects Capable of Being Added Using +

The special method corresponding to the + operator is the __add__() method. Adding a custom definition of __add__() changes the behavior of the operator. It is recommended that __add__() returns a new instance of the class instead of modifying the calling instance itself. You’ll see this behavior quite commonly in Python:

>>>
>>> a = 'Real'
>>> a + 'Python'  # Gives new str instance
'RealPython'
>>> a  # Values unchanged
'Real'
>>> a = a + 'Python'  # Creates new instance and assigns a to it
>>> a
'RealPython'

You can see above that using the + operator on a str object actually returns a new str instance, keeping the value of the calling instance (a) unmodified. To change it, we need to explicitly assign the new instance to a.

Let’s implement the ability to append new items to our cart in the Order class using the operator. We’ll follow the recommended practice and make the operator return a new Order instance that has our required changes instead of making the changes directly to our instance:

>>>
>>> class Order:
...     def __init__(self, cart, customer):
...         self.cart = list(cart)
...         self.customer = customer
...
...     def __add__(self, other):
...         new_cart = self.cart.copy()
...         new_cart.append(other)
...         return Order(new_cart, self.customer)
...
>>> order = Order(['banana', 'apple'], 'Real Python')

>>> (order + 'orange').cart  # New Order instance
['banana', 'apple', 'mango']
>>> order.cart  # Original instance unchanged
['banana', 'apple']

>>> order = order + 'mango'  # Changing the original instance
>>> order.cart
['banana', 'apple', 'mango']

Similarly, you have the __sub__(), __mul__(), and other special methods which define the behavior of -, *, and so on. These methods should return a new instance of the class as well.

Shortcuts: the += Operator

The += operator stands as a shortcut to the expression obj1 = obj1 + obj2. The special method corresponding to it is __iadd__(). The __iadd__() method should make changes directly to the self argument and return the result, which may or may not be self. This behavior is quite different from __add__() since the latter creates a new object and returns that, as you saw above.

Roughly, any += use on two objects is equivalent to this:

>>> result = obj1 + obj2
>>> obj1 = result

Here, result is the value returned by __iadd__(). The second assignment is taken care of automatically by Python, meaning that you do not need to explicitly assign obj1 to the result as in the case of obj1 = obj1 + obj2.

Let’s make this possible for the Order class so that new items can be appended to the cart using +=:

>>>
>>> class Order:
...     def __init__(self, cart, customer):
...         self.cart = list(cart)
...         self.customer = customer
...
...     def __iadd__(self, other):
...         self.cart.append(other)
...         return self
...
>>> order = Order(['banana', 'apple'], 'Real Python')
>>> order += 'mango'
>>> order.cart
['banana', 'apple', 'mango']

As can be seen, any change is made directly to self and it is then returned. What happens when you return some random value, like a string or an integer?

>>>
>>> class Order:
...     def __init__(self, cart, customer):
...         self.cart = list(cart)
...         self.customer = customer
...
...     def __iadd__(self, other):
...         self.cart.append(other)
...         return 'Hey, I am string!'
...
>>> order = Order(['banana', 'apple'], 'Real Python')
>>> order += 'mango'
>>> order
'Hey, I am string!'

Even though the relevant item was appended to the cart, the value of order changed to what was returned by __iadd__(). Python implicitly handled the assignment for you. This can lead to surprising behavior if you forget to return something in your implementation:

>>>
>>> class Order:
...     def __init__(self, cart, customer):
...         self.cart = list(cart)
...         self.customer = customer
...
...     def __iadd__(self, other):
...         self.cart.append(other)
...
>>> order = Order(['banana', 'apple'], 'Real Python')
>>> order += 'mango'
>>> order  # No output
>>> type(order)
NoneType

Since all Python functions (or methods) return None implicitly, order is reassigned to None and the REPL session doesn’t show any output when order is inspected. Looking at the type of order, you see that it is now NoneType. Therefore, always make sure that you’re returning something in your implementation of __iadd__() and that it is the result of the operation and not anything else.

Similar to __iadd__(), you have __isub__(), __imul__(), __idiv__() and other special methods which define the behavior of -=, *=, /=, and others alike.

Indexing and Slicing Your Objects Using []

The [] operator is called the indexing operator and is used in various contexts in Python such as getting the value at an index in sequences, getting the value associated with a key in dictionaries, or obtaining a part of a sequence through slicing. You can change its behavior using the __getitem__() special method.

Let’s configure our Order class so that we can directly use the object and obtain an item from the cart:

>>>
>>> class Order:
...     def __init__(self, cart, customer):
...         self.cart = list(cart)
...         self.customer = customer
...
...     def __getitem__(self, key):
...         return self.cart[key]
...
>>> order = Order(['banana', 'apple'], 'Real Python')
>>> order[0]
'banana'
>>> order[-1]
'apple'

You’ll notice that above, the name of the argument to __getitem__() is not index but key. This is because the argument can be of mainly three forms: an integer value, in which case it is either an index or a dictionary key, a string value, in which case it is a dictionary key, and a slice object, in which case it will slice the sequence used by the class. While there are other possibilities, these are the ones most commonly encountered.

Since our internal data structure is a list, we can use the [] operator to slice the list, as in this case, the key argument will be a slice object. This is one of the biggest advantages of having a __getitem__() definition in your class. As long as you’re using data structures that support slicing (lists, tuples, strings, and so on), you can configure your objects to directly slice the structure:

>>>
>>> order[1:]
['apple']
>>> order[::-1]
['apple', 'banana']

Reverse Operators: Making Your Classes Mathematically Correct

While defining the __add__(), __sub__(), __mul__(), and similar special methods allows you to use the operators when your class instance is the left-hand side operand, the operator will not work if the class instance is the right-hand side operand:

>>>
>>> class Mock:
...     def __init__(self, num):
...         self.num = num
...     def __add__(self, other):
...         return Mock(self.num + other)
...
>>> mock = Mock(5)
>>> mock = mock + 6
>>> mock.num
11

>>> mock = 6 + Mock(5)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'int' and 'Mock'

If your class represents a mathematical entity like a vector, a coordinate, or a complex number, applying the operators should work in both the cases since it is a valid mathematical operation.

Moreover, if the operators work only when the instance is the left operand, we are violating the fundamental principle of commutativity in many cases. Therefore, to help you make your classes mathematically correct, Python provides you with reverse special methods such as __radd__(), __rsub__(), __rmul__(), and so on.

These handle calls such as x + obj, x - obj, and x * obj, where x is not an instance of the concerned class. Just like __add__() and the others, these reverse special methods should return a new instance of class with the changes of the operation rather than modifying the calling instance itself.

Let’s configure __radd__() in the Order class in such a way that it will append something at the front of the cart. This can be used in cases where the cart is organized in terms of the priority of the orders:

>>>
>>> class Order:
...     def __init__(self, cart, customer):
...         self.cart = list(cart)
...         self.customer = customer
...
...     def __add__(self, other):
...         new_cart = self.cart.copy()
...         new_cart.append(other)
...         return Order(new_cart, self.customer)
...
...     def __radd__(self, other):
...         new_cart = self.cart.copy()
...         new_cart.insert(0, other)
...         return Order(new_cart, self.customer)
...
>>> order = Order(['banana', 'apple'], 'Real Python')

>>> order = order + 'orange'
>>> order.cart
['banana', 'apple', 'orange']

>>> order = 'mango' + order
>>> order.cart
['mango', 'banana', 'apple', 'orange']

A Complete Example

To drive all these points home, it’s better to look at an example class which implements these operators together.

Let’s reinvent the wheel and implement our own class to represent complex numbers, CustomComplex. Objects of our class will support a variety of built-in functions and operators, making them behave very similar to the built-in complex numbers class:

from math import hypot, atan, sin, cos

class CustomComplex:
    def __init__(self, real, imag):
        self.real = real
        self.imag = imag

The constructor handles only one kind of call, CustomComplex(a, b). It takes positional arguments, representing the real and imaginary parts of the complex number.

Let’s define two methods inside the class, conjugate() and argz(), which will give us the complex conjugate and the argument of a complex number respectively:

def conjugate(self):
    return self.__class__(self.real, -self.imag)

def argz(self):
    return atan(self.imag / self.real)

Next, we configure abs() to return the modulus of a complex number:

def __abs__(self):
    return hypot(self.real, self.imag)

We will follow the recommended distinction between __repr__() and __str__() and use the first for the parsable string representation and the second for a “pretty” representation.

The __repr__() method will simply return CustomComplex(a, b) in a string so that we can call eval() to recreate the object, while the __str__() method will return the complex number in brackets, as (a+bj):

def __repr__(self):
    return f"{self.__class__.__name__}({self.real}, {self.imag})"

def __str__(self):
    return f"({self.real}{self.imag:+}j)"

Mathematically, it is possible to add any two complex numbers or add a real number to a complex number. Let’s configure the + operator in such a way that it works for both cases.

The method will check the type of the right-hand side operator. In case it is an int or a float, it will increment only the real part (since any real number, a, is equivalent to a+0j), while in the case of another complex number, it will change both the parts:

def __add__(self, other):
    if isinstance(other, float) or isinstance(other, int):
        real_part = self.real + other
        imag_part = self.imag

    if isinstance(other, CustomComplex):
        real_part = self.real + other.real
        imag_part = self.imag + other.imag

    return self.__class__(real_part, imag_part)

Similarly, we define the behavior for - and *:

def __sub__(self, other):
    if isinstance(other, float) or isinstance(other, int):
        real_part = self.real - other
        imag_part = self.imag

    if isinstance(other, CustomComplex):
        real_part = self.real - other.real
        imag_part = self.imag - other.imag

    return self.__class__(real_part, imag_part)

def __mul__(self, other):
    if isinstance(other, int) or isinstance(other, float):
        real_part = self.real * other
        imag_part = self.imag * other

    if isinstance(other, CustomComplex):
        real_part = (self.real * other.real) - (self.imag * other.imag)
        imag_part = (self.real * other.imag) + (self.imag * other.real)

    return self.__class__(real_part, imag_part)

Since both addition and multiplication are commutative, we can define their reverse operators by calling __add__() and __mul__() in __radd__() and __rmul__() respectively. On the other hand, the behavior of __rsub__() needs to be defined since subtraction is not commutative:

def __radd__(self, other):
    return self.__add__(other)

def __rmul__(self, other):
    return self.__mul__(other)

def __rsub__(self, other):
    # x - y != y - x
    if isinstance(other, float) or isinstance(other, int):
        real_part = other - self.real
        imag_part = -self.imag

    return self.__class__(real_part, imag_part)

Now, we take care of the two operators, == and !=. The special methods used for them are __eq__() and __ne__(), respectively. Two complex numbers are said to be equal if their corresponding real and imaginary parts are both equal. They are said to be unequal when either one of these are unequal:

def __eq__(self, other):
    # Note: generally, floats should not be compared directly
    # due to floating-point precision
    return (self.real == other.real) and (self.imag == other.imag)

def __ne__(self, other):
    return (self.real != other.real) or (self.imag != other.imag)

It is also possible to raise a complex number to any power using a simple formula. We configure the behavior for both the built-in pow() and the ** operator using the __pow__() special method:

def __pow__(self, other):
    r_raised = abs(self) ** other
    argz_multiplied = self.argz() * other

    real_part = round(r_raised * cos(argz_multiplied))
    imag_part = round(r_raised * sin(argz_multiplied))

    return self.__class__(real_part, imag_part)

Let’s create two instances of this class, one having a positive imaginary part and one having a negative imaginary part:

>>>
>>> a = CustomComplex(1, 2)
>>> b = CustomComplex(3, -4)

String representations:

>>>
>>> a
CustomComplex(1, 2)
>>> b
CustomComplex(3, -4)
>>> print(a)
(1+2j)
>>> print(b)
(3-4j)

Recreating the object using eval() with repr():

>>>
>>> b_copy = eval(repr(b))
>>> type(b_copy), b_copy.real, b_copy.imag
(__main__.CustomComplex, 3, -4)

Addition, subtraction, and multiplication:

>>>
>>> a + b
CustomComplex(4, -2)
>>> a - b
CustomComplex(-2, 6)
>>> a + 5
CustomComplex(6, 2)
>>> 3 - a
CustomComplex(2, -2)
>>> a * 6
CustomComplex(6, 12)
>>> a * (-6)
CustomComplex(-6, -12)

Equality and inequality checks:

>>>
>>> a == CustomComplex(1, 2)
True
>>> a ==  b
False
>>> a != b
True
>>> a != CustomComplex(1, 2)
False

Finally, raising a complex number to some power:

>>>
>>> a ** 2
CustomComplex(-3, 4)
>>> b ** 5
CustomComplex(-237, 3116)

As you can see, objects of our custom class behave and look like those of a built-in class and are very Pythonic. The full example code for this class is embedded below.

from math import hypot, atan, sin, cos

class CustomComplex():
    """
    A class to represent a complex number, a+bj.
    Attributes:
        real - int, representing the real part
        imag - int, representing the imaginary part

    Implements the following:

    * Addition with a complex number or a real number using `+`
    * Multiplication with a complex number or a real number using `*`
    * Subtraction of a complex number or a real number using `-`
    * Calculation of absolute value using `abs`
    * Raise complex number to a power using `**`
    * Nice string representation using `__repr__`
    * Nice user-end viewing using `__str__`

    Notes:
        * The constructor has been intentionally kept simple
        * It is configured to support one kind of call:
            CustomComplex(a, b)
        * Error handling was avoided to keep things simple
    """

    def __init__(self, real, imag):
        """
        Initializes a complex number, setting real and imag part
        Arguments:
            real: Number, real part of the complex number
            imag: Number, imaginary part of the complex number
        """
        self.real = real
        self.imag = imag

    def conjugate(self):
        """
        Returns the complex conjugate of a complex number
        Return:
            CustomComplex instance
        """
        return CustomComplex(self.real, -self.imag)

    def argz(self):
        """
        Returns the argument of a complex number
        The argument is given by:
            atan(imag_part/real_part)
        Return:
            float
        """
        return atan(self.imag / self.real)

    def __abs__(self):
        """
        Returns the modulus of a complex number
        Return:
            float
        """
        return hypot(self.real, self.imag)

    def __repr__(self):
        """
        Returns str representation of an instance of the 
        class. Can be used with eval() to get another 
        instance of the class
        Return:
            str
        """
        return f"CustomComplex({self.real}, {self.imag})"


    def __str__(self):
        """
        Returns user-friendly str representation of an instance 
        of the class
        Return:
            str
        """
        return f"({self.real}{self.imag:+}j)"

    def __add__(self, other):
        """
        Returns the addition of a complex number with
        int, float or another complex number
        Return:
            CustomComplex instance
        """
        if isinstance(other, float) or isinstance(other, int):
            real_part = self.real + other
            imag_part = self.imag

        if isinstance(other, CustomComplex):
            real_part = self.real + other.real
            imag_part = self.imag + other.imag

        return CustomComplex(real_part, imag_part)

    def __sub__(self, other):
        """
        Returns the subtration from a complex number of
        int, float or another complex number
        Return:
            CustomComplex instance
        """
        if isinstance(other, float) or isinstance(other, int):
            real_part = self.real - other
            imag_part = self.imag

        if isinstance(other, CustomComplex):
            real_part = self.real - other.real
            imag_part = self.imag - other.imag

        return CustomComplex(real_part, imag_part)

    def __mul__(self, other):
        """
        Returns the multiplication of a complex number with
        int, float or another complex number
        Return:
            CustomComplex instance
        """
        if isinstance(other, int) or isinstance(other, float):
            real_part = self.real * other
            imag_part = self.imag * other

        if isinstance(other, CustomComplex):
            real_part = (self.real * other.real) - (self.imag * other.imag)
            imag_part = (self.real * other.imag) + (self.imag * other.real)

        return CustomComplex(real_part, imag_part)

    def __radd__(self, other):
        """
        Same as __add__; allows 1 + CustomComplex('x+yj')
        x + y == y + x
        """
        pass

    def __rmul__(self, other):
        """
        Same as __mul__; allows 2 * CustomComplex('x+yj')
        x * y == y * x
        """
        pass

    def __rsub__(self, other):
        """
        Returns the subtraction of a complex number from
        int or float
        x - y != y - x
        Subtration of another complex number is not handled by __rsub__
        Instead, __sub__ handles it since both sides are instances of
        this class
        Return:
            CustomComplex instance
        """
        if isinstance(other, float) or isinstance(other, int):
            real_part = other - self.real
            imag_part = -self.imag

        return CustomComplex(real_part, imag_part)

    def __eq__(self, other):
        """
        Checks equality of two complex numbers
        Two complex numbers are equal when:
            * Their real parts are equal AND
            * Their imaginary parts are equal
        Return:
            bool
        """
        # note: comparing floats directly is not a good idea in general
        # due to floating-point precision
        return (self.real == other.real) and (self.imag == other.imag)

    def __ne__(self, other):
        """
        Checks inequality of two complex numbers
        Two complex numbers are unequal when:
            * Their real parts are unequal OR
            * Their imaginary parts are unequal
        Return:
            bool
        """
        return (self.real != other.real) or (self.imag != other.imag)

    def __pow__(self, other):
        """
        Raises a complex number to a power
        Formula:
            z**n = (r**n)*[cos(n*agrz) + sin(n*argz)j], where
            z = complex number
            n = power
            r = absolute value of z
            argz = argument of z
        Return:
            CustomComplex instance
        """
        r_raised = abs(self) ** other
        argz_multiplied = self.argz() * other

        real_part = round(r_raised * cos(argz_multiplied))
        imag_part = round(r_raised * sin(argz_multiplied))

        return CustomComplex(real_part, imag_part)

Recap and Resources

In this tutorial, you learned about the Python Data Model and how the Data Model can be used to build Pythonic classes. You learned about changing the behavior of built-in functions such as len(), abs(), str(), bool(), and so on. You also learned about changing the behavior of built-in operators like +, -, *, **, and so forth.

After reading this, you can confidently create classes that make use of the best idiomatic features of Python and make your objects Pythonic!

For more information on the Data Model, and function and operator overloading, take a look at these resources:

🐍 Python Tricks 💌

Get a short & sweet Python Trick delivered to your inbox every couple of days. No spam ever. Unsubscribe any time. Curated by the Real Python team.

Python Tricks Dictionary Merge

About Malay Agarwal

Malay Agarwal

A tech geek with a philosophical mind and a hand that can wield a pen.

» More about Malay

Each tutorial at Real Python is created by a team of developers so that it meets our high quality standards. The team members who worked on this tutorial are:

What Do You Think?

Real Python Comment Policy: The most useful comments are those written with the goal of learning from or helping out other readers—after reading the whole article and all the earlier comments. Complaints and insults generally won’t make the cut here.

Keep Reading