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.

**Fun fact:** Due to the naming convention used in these methods, they are also called **dunder methods**, short for “dunder something dunder,” which is the shorter version of * double underscore something double underscore*.

## 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)'

**Note:** In cases where the `__str__()`

method is not defined, Python uses the `__repr__()`

method to print the object, as well as to represent the object when `str()`

is called on it. If both the methods are missing, it defaults to `<__main__.Vector ...>`

. But `__repr__()`

is the only method that is used to display the object in an interactive session. Absence of it in the class yields `<__main__.Vector ...>`

.

Also, while this distinction between `__str__()`

and `__repr__()`

is the recommended behavior, many of the popular libraries ignore this distinction and use the two methods interchangeably.

Here’s a recommended article on `__repr__()`

and `__str__()`

by our very own Dan Bader: Python String Conversion 101: Why Every Class Needs a “repr”.

### 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

**Note:** When the `__bool__()`

special method is not implemented in a class, the value returned by `__len__()`

is used as the truth value, where a non-zero value indicates `True`

and a zero value indicates `False`

. In case both the methods are not implemented, all instances of the class are considered to be `True`

.

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:

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.

**Note:** When `__iadd__()`

or its friends are missing from your class definition but you still use their operators on your objects, Python uses `__add__()`

and its friends to get the result of the operation and assigns that to the calling instance. Generally speaking, it is safe to not implement `__iadd__()`

and its friends in your classes as long as `__add__()`

and its friends work properly (return something which is the result of the operation).

The Python documentation has a good explanation of these methods. Also, take a look at this example which shows the caveats involved with `+=`

and the others when working with immutable types.

### 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']

**Note:** There is a similar `__setitem__()`

special method that is used to define the behavior of `obj[x] = y`

. This method takes two arguments in addition to `self`

, generally called `key`

and `value`

, and can be used to change the value at `key`

to `value`

.

### 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)

**Note:** `__class__`

is not a special method but a class attribute which is present by default. It has a reference to the class. By using it here, we are obtaining that and then calling the constructor in the usual manner. In other words, this is equivalent to `CustomComplex(real, imag)`

. This is done here to avoid refactoring the code if the name of the class changes someday.

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): self.__add__(other) def __rmul__(self, other): 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)

**Note:** You might have noticed that we didn’t add a construct to handle a `CustomComplex`

instance here. This is because, in such a case, both the operands are instances of our class, and `__rsub__()`

won’t be responsible for handling the operation. Instead, `__sub__()`

will be called. This is a subtle but important detail.

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)

**Note:** The Floating-Point Guide is an article that talks about comparing floats and floating-point precision. It highlights the caveats involved in comparing floats directly, which is something we’re doing here.

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)

**Note:** Take a close look at the definition of the method. We are calling `abs()`

to obtain the modulus of the complex number. So, once you’ve defined the special method for a particular function or operator in your class, it can be used in other methods of the same class.

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: