Arithmetic Dunder Methods PREMIUM

Trey Hunner smiling in a t-shirt against a yellow wall
Trey Hunner
8 min. read Python 3.10—3.14
Tags

How can you control the arithmetic operators on your Python objects, like +, *, and /?

It's all about dunder methods

You can overload arithmetic operators by implementing the right dunder methods.

When Python sees a + operator between two objects, it will call the __add__ method on the first object, passing in the second object:

>>> x = "Hi"
>>> y = "there"
>>> x + y
'Hithere'
>>> x.__add__(y)
'Hithere'

For multiplication, Python will call the __mul__ method:

>>> a = 4
>>> b = 2
>>> a * b
8
>>> a.__mul__(b)
8

And for subtraction, Python calls __sub__:

>>> a - b
2
>>> a.__sub__(b)
2

Each of Python's arithmetic operators has a corresponding dunder method:

Operator Dunder Method
+ __add__
- __sub__
* __mul__
/ __truediv__
% __mod__
// __floordiv__
** __pow__
@ __matmul__

That // operator is integer division and that @ operator is matrix multiplication.

But those are just the operators that are common to see for numeric arithmetic. Python also has operators for bitwise arithmetic:

Operator Dunder Method
& __and__
| __or__
^ __xor__

Those bitwise operators are for bitwise AND, OR, and XOR, but those operators are more commonly seen outside of bitwise settings. For example, sets in Python use those three operators to represent intersection, union, and symmetric difference.

There are actually two dunder methods per operator

These arithmetic operators don't just have one dunder method that's called. Each of these operators actually relies on two separate dunder methods to work: there's one method for left-hand operations and another one for right-hand operations.

To see the difference, let's look at the multiplication operator with strings. Multiplying a string by an integer, will self-concatenate that string:

>>> x = "Hi"
>>> n = 5
>>> x * n
'HiHiHiHiHi'

That's the same as if we called the __mul__ method on the string, passing in the number:

>>> x.__mul__(n)
'HiHiHiHiHi'

But what if we multiply the number by the string? Well, if we call __mul__ on our integer and we pass it a string, we'll see NotImplemented:

>>> n.__mul__(x)
NotImplemented

And yet, multiplying the integer by the string does work:

>>> n * x
'HiHiHiHiHi'

This operation still works thanks to the __rmul__ method on strings:

>>> x.__rmul__(n)
'HiHiHiHiHi'

The __rmul__ method is used by Python to ask the string to multiply itself by the number from the right-hand side.

Every arithmetic operation has both a left-hand operation, which is attempted first, and a right-hand operations which is attempted second.

Operation Left-Hand Method Right-Hand Method
x + y __add__ __radd__
x - y __sub__ __rsub__
x * y __mul__ __rmul__
x / y __truediv__ __rtruediv__
x % y __mod__ __rmod__
x // y __floordiv__ __rfloordiv__
x ** y __pow__ __rpow__
x @ y __matmul__ __rmatmul__
x & y __and__ __rand__
x | y __or__ __ror__
x ^ y __xor__ __rxor__

But how does this really work? And when do you need a right-hand operation?

Let's try to implement a couple operators and see what it's like.

Implementing left-hand dunder methods

Let's make an object that can be concatenated to other objects of the same type:

class Money:
    def __init__(self, amount, currency):
        self.amount = amount
        self.currency = currency

    def __repr__(self):
        return f"Money({self.amount:.2f}, {self.currency!r})"

    def __add__(self, other):
        if self.currency == other.currency:
            return Money(self.amount + other.amount, self.currency)
        raise ValueError("Cannot add amounts in different currencies")

We've implemented a __add__ method on this class that allows us to add two instances of this class together:

>>> a = Money(50, "USD")
>>> b = Money(20, "USD")
>>> a + b
Money(70.00, 'USD')

But what if we tried adding an instance of this class to a different type of object?

>>> a + 10
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 10, in __add__
AttributeError: 'int' object has no attribute 'currency'

We got an AttributeError. It would have been clearer to see a TypeError instead. That's what we usually see if we try use operators between two types of objects that don't support that operation:

>>> [1, 2] + 3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can only concatenate list (not "int") to list

We could check the type of the given object and then raise a TypeError ourselves. But Python instead recommends that we return a special NotImplemented object.

class Money:
    def __init__(self, amount, currency):
        self.amount = amount
        self.currency = currency

    def __repr__(self):
        return f"{self.currency} {self.amount:.2f}"

    def __add__(self, other):
        if not isinstance(other, Money):
            return NotImplemented
        if self.currency != other.currency:
            raise ValueError("Cannot add amounts in different currencies")
        return Money(self.amount + other.amount, self.currency)

That tells Python to go check the other object before deciding whether the operation is unsupported. If the other object doesn't seem to support the operation either, then a TypeError is raised

>>> a = Money(50, "USD")
>>> a + 10
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'Money' and 'int'

So we're not meant to raise a TypeError: it's usually best to return NotImplemented instead.

But... why?

Why NotImplemented?

Why return NotImplemented? What's the downside of raising a TypeError?

Well, integers in Python don't know how to divide themselves by floating point numbers:

>>> a = 10
>>> b = 2.0
>>> a.__truediv__(b)
NotImplemented

But we do want to be able to work with integers and floating point numbers together. So instead of an integer raising a TypeError whenever its dunder methods are given an object it doesn't know how to handle, it returns NotImplemented.

That signals to Python that it's meant to check whether the right-hand object has an appropriate dunder method for the same operation from the right-hand side... and in our case, it does:

>>> b.__rtruediv__(a)
5.0

Floating point numbers know how to divide an integer by a floating point number. So the operation works as expected:

>>> a / b
5.0

It's very important to return NotImplemented for all types of objects that your dunder arithmetic dunder method doesn't know how to handle.

Are right-hand dunder methods always necessary?

When do you need to implement these right-hand side operations? Does every operation on every object need both a left-operation and a right-hand one?

The short answer is that right-hand operations are only necessary if your object is meant to work with objects of a different type.

For example, the __add__ we implemented before is only meant to allow for addition/concatenation between objects of the same type:

    def __add__(self, other):
        if not isinstance(other, Money):
            return NotImplemented
        if self.currency != other.currency:
            raise ValueError("Cannot add amounts in different currencies")
        return Money(self.amount + other.amount, self.currency)

So we don't need a __radd__ method because all valid addition operation with a Money object on the right-hand side will also have a Money object on the left-hand side:

>>> x = Money(30, "USD")
>>> y = Money(63, "USD")
>>> y + y
Money(93.00, 'USD')

What if we wanted to implement multiplication between our object type and numbers?

If we only implement __mul__:

    def __mul__(self, other):
        if not isinstance(other, (int, float)):
            return NotImplemented
        return Money(self.amount * other, self.currency)

Multiplication between a number a Money object will work if the Money object is on the left-hand side:

>>> a = Money(50, "USD")
>>> a * 3
Money(150.00, 'USD')

But it won't work if the number is on the left-hand side:

>>> 3 * a
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for *: 'int' and 'Money'

To support both of these operations, we need to implement a __rmul__ method:

class Money:
    def __init__(self, amount, currency):
        self.amount = amount
        self.currency = currency

    def __repr__(self):
        return f"{self.currency} {self.amount:.2f}"

    def __add__(self, other):
        if not isinstance(other, Money):
            return NotImplemented
        if self.currency != other.currency:
            raise ValueError("Cannot add amounts in different currencies")
        return Money(self.amount + other.amount, self.currency)

    def __mul__(self, other):
        if not isinstance(other, (int, float)):
            return NotImplemented
        return Money(self.amount * other, self.currency)

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

Now multiplication works regardless of which way we do it:

>>> a = Money(50, "USD")
>>> a * 3
Money(150.00, 'USD')
>>> 2 * a
Money(100.00, 'USD')

So right-hand dunder methods are often necessary for supporting operations between two different types of objects. If you only need to support operations with the same exact object type on both sides of the operator, then you probably don't need to implement a right-hand side operation.

Unary operators are easy

We've been talking about binary arithmetic operators. By "binary", I mean operators that go between two objects.

Python also has some unary arithmetic operators. For example, you can use - to negate a number or to negate some other types of objects:

>>> n = 3
>>> -n
-3

The unary - operation is implemented by the __neg__ method:

class Money:
    def __init__(self, amount, currency):
        self.amount = amount
        self.currency = currency

    def __repr__(self):
        return f"{self.currency} {self.amount:.2f}"

    def __add__(self, other):
        if not isinstance(other, Money):
            return NotImplemented
        if self.currency != other.currency:
            raise ValueError("Cannot add amounts in different currencies")
        return Money(self.amount + other.amount, self.currency)

    def __mul__(self, other):
        if not isinstance(other, (int, float)):
            return NotImplemented
        return Money(self.amount * other, self.currency)

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

    def __neg__(self):
        return Money(-self.amount)

So with that dunder method implemented, we can now negate Money objects:

>>> a = Money(50, "USD")
>>> -a
Money(-50, 'USD')

Supporting unary arithmetic operations is a bit easier than binary ones because there is no other object to worry about.

Here are all of Python's unary arithmetic operations:

Operator Description Dunder Method
- Unary negative __neg__
+ Unary positive __pos__
~ Bitwise NOT (invert) __invert__

Remember NotImplemented and consider right-hand operations

When implementing arithmetic dunder methods in Python, remember to return NotImplemented for object types that you don't know how to work with. That gives other objects the opportunity to extend the behavior of different operations with your object.

Also remember that if your operation is meant to work with a different types of object than the one you're implementing, you probably need to implement a dunder method for the right-hand operation as well.

This is a free preview of a premium article. You have 1 preview remaining.