Python Operator Overloading: Arithmetic Operators

In Python, operator overloading allows you to define how built-in operators like +, -, *, or / behave when used with objects of your own classes. Instead of relying on method calls like vector1.add(vector2), you can write expressions such as vector1 + vector2, making your code more readable and intuitive.

This feature is especially useful when working with classes that represent numeric or mathematical entities—like vectors, complex numbers, matrices, or even units of currency. By defining special methods like __add__() or __mul__(), your objects can participate in arithmetic expressions just like built-in types.

For example, imagine a simple 2D vector class:

Vector(1, 2) + Vector(3, 4)

This expression looks natural and clear. Thanks to operator overloading, Python can evaluate it using logic you define inside your class, giving you the power to write expressive, elegant code tailored to your specific domain.

The Basics: Defining a Class That Supports Operators

Before we dive into individual operators, let’s first understand how operator overloading works by creating a simple class. We’ll use a Money class to represent amounts in a specific currency—something that shows up in real-world applications like budgeting tools or e-commerce systems.

Here’s how the class looks without any operator overloading:

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

wallet1 = Money(50, "USD")
wallet2 = Money(30, "USD")

# Try to add them
result = wallet1 + wallet2  # This will raise a TypeError

If you try to run this, Python will raise a TypeError, saying that the + operator is not supported between Money instances. That’s because Python doesn’t know how to add two custom Money objects—unless you tell it.

To fix this, you define a magic method—a special method with double underscores—such as __add__(). These methods let Python know what to do when operators are used with your class.

Here’s how to implement the __add__() method:

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

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

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

wallet1 = Money(50, "USD")
wallet2 = Money(30, "USD")

total = wallet1 + wallet2
print(total)  # Output: 80 USD

Now, the + operator works naturally. Behind the scenes, Python automatically calls wallet1.__add__(wallet2), which returns a new Money object representing the total.

This is the foundation of operator overloading. You define how your class behaves when it encounters operators, making your custom types feel just like built-in ones.

Overloading + with __add__

In Python, the + operator calls the special method __add__() when used between objects. Overloading this method allows you to define exactly what should happen when two instances of your class are added together.

Let’s continue with our Money class, which represents an amount and its currency. A natural operation is to add two monetary values—but only if they share the same currency. Here’s how you can make + work using __add__:

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

    def __add__(self, other):
        if self.currency != other.currency:
            raise ValueError("Can't add different currencies")
        return Money(self.amount + other.amount, self.currency)

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

Now, you can use the + operator naturally:

wallet1 = Money(100, "EUR")
wallet2 = Money(50, "EUR")
total = wallet1 + wallet2

print(total)  # Output: 150 EUR

But if you try to add two amounts in different currencies, Python will raise a helpful error:

usd = Money(75, "USD")
eur = Money(20, "EUR")

# This will raise ValueError
result = usd + eur

By defining __add__, you’re not just enabling arithmetic—you’re also embedding domain rules (like currency matching) directly into the logic. This keeps your code readable, expressive, and safe.

Overloading - with __sub__

Just like +, the - operator can be overloaded using the special method __sub__(). This lets you define what it means to subtract one instance of your class from another.

For example, with the Money class, subtraction is useful for operations like deducting an expense from a budget. As with addition, you’ll want to ensure the currencies match before proceeding.

Here’s how to implement subtraction:

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

    def __sub__(self, other):
        if self.currency != other.currency:
            raise ValueError("Can't subtract different currencies")
        return Money(self.amount - other.amount, self.currency)

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

Now you can subtract money amounts naturally:

salary = Money(3000, "USD")
rent = Money(1200, "USD")
remaining = salary - rent

print(remaining)  # Output: 1800 USD

Attempting to subtract different currencies will raise an error:

usd = Money(100, "USD")
eur = Money(50, "EUR")

# Raises ValueError
change = usd - eur

Overloading __sub__ helps your class support intuitive arithmetic, while also enforcing important rules that protect against incorrect or nonsensical operations.

Overloading * with __mul__

The * operator is overloaded using the __mul__() method. This is especially useful when you want to scale a value—like multiplying a Money amount by a number to calculate totals, apply discounts, or project future earnings.

In the case of our Money class, multiplying by an integer or float makes sense. For example, multiplying a salary by 12 to get a yearly total.

Here’s how to implement it:

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

    def __mul__(self, factor):
        if not isinstance(factor, (int, float)):
            raise TypeError("Can only multiply Money by int or float")
        return Money(self.amount * factor, self.currency)

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

Now you can do things like this:

monthly_salary = Money(2500, "USD")
yearly_salary = monthly_salary * 12

print(yearly_salary)  # Output: 30000 USD

If someone tries to multiply two Money objects, or use an unsupported type, Python will raise a clear error:

bonus = Money(1000, "USD")

# Raises TypeError
invalid = monthly_salary * bonus

This approach makes your custom class both safe and expressive—behaving just like built-in types in the situations where it makes sense.

Overloading / with __truediv__

The / operator uses the __truediv__() method under the hood. In real-world terms, dividing a Money object by a number can represent splitting an amount—for example, dividing a bill among friends or allocating a budget across multiple departments.

Let’s update our Money class to support this behavior:

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

    def __truediv__(self, divisor):
        if not isinstance(divisor, (int, float)):
            raise TypeError("Can only divide Money by int or float")
        if divisor == 0:
            raise ZeroDivisionError("Division by zero is not allowed")
        return Money(self.amount / divisor, self.currency)

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

Here’s how it works in practice:

total_bill = Money(150, "USD")
per_person = total_bill / 3

print(per_person)  # Output: 50.00 USD

The method includes type checking to ensure you’re only dividing by numbers, and it guards against dividing by zero—just like Python’s built-in types.

This makes your class safe, practical, and perfectly suited for common financial calculations.

Overloading // with __floordiv__

The floor division operator // performs division but rounds down to the nearest whole number. This is useful in scenarios like splitting a bill where you want to avoid fractional cents or allocate whole units.

To support this, we overload the __floordiv__() method in our Money class. This method will divide the amount and then apply floor division, returning a new Money object with the rounded-down value.

Here’s the implementation:

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

    def __floordiv__(self, divisor):
        if not isinstance(divisor, (int, float)):
            raise TypeError("Can only floor divide Money by int or float")
        if divisor == 0:
            raise ZeroDivisionError("Division by zero is not allowed")
        return Money(self.amount // divisor, self.currency)

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

Example usage:

total_amount = Money(100, "USD")
share = total_amount // 3

print(share)  # Output: 33 USD

Here, 100 // 3 equals 33, as the fractional part is discarded. This is practical when splitting money into whole units, avoiding fractional pennies.

By defining __floordiv__, your Money class can handle both precise division (/) and rounded-down division (//), covering a broad range of real-world use cases.

Overloading % with __mod__

The modulo operator % returns the remainder after division. In financial contexts, this can help calculate leftover amounts when splitting money.

For example, if you divide \$100 by 3, each person gets \$33, and there’s a remainder of \$1 left undistributed. Overloading the __mod__() method lets your Money class capture this remainder.

Here’s how to implement it:

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

    def __mod__(self, divisor):
        if not isinstance(divisor, (int, float)):
            raise TypeError("Can only modulo Money by int or float")
        return Money(self.amount % divisor, self.currency)

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

Example usage:

total_amount = Money(100, "USD")
remainder = total_amount % 3

print(remainder)  # Output: 1 USD

By supporting modulo, your class can handle both how much each share is worth and what remains—helpful in many real-world scenarios involving money distribution.

This completes the set of basic arithmetic operations that let your custom class behave naturally and intuitively in Python expressions.

Overloading ** with __pow__

The exponentiation operator ** is used to raise numbers to a power. Overloading this operator with the __pow__() method can be useful in many contexts—such as performing element-wise power operations on vectors.

Consider a Vector2D class that holds two coordinates, x and y. Using ** to raise each coordinate to a given power lets you apply mathematical transformations easily.

Here’s how you might implement it:

class Vector2D:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __pow__(self, power):
        if not isinstance(power, (int, float)):
            raise TypeError("Power must be an int or float")
        return Vector2D(self.x ** power, self.y ** power)

    def __str__(self):
        return f"Vector2D({self.x}, {self.y})"

Example usage:

vec = Vector2D(2, 3)
squared = vec ** 2

print(squared)  # Output: Vector2D(4, 9)

This example raises each component of the vector to the power of 2. Such behavior is helpful in graphics, physics calculations, or any domain where vector transformations are common.

By overloading __pow__, you can extend your classes with flexible, readable power operations.

Overloading @ with __matmul__

The @ operator is designed for matrix multiplication and is especially useful when working with 2D arrays, transformations in graphics, or linear algebra.

By overloading __matmul__(), you can define how your custom matrix objects multiply together. This lets you write clean, natural expressions for matrix operations.

Here’s an example using simple 2×2 matrices:

class Matrix2x2:
    def __init__(self, values):
        if len(values) != 2 or any(len(row) != 2 for row in values):
            raise ValueError("Values must be a 2x2 list")
        self.values = values

    def __matmul__(self, other):
        a, b = self.values, other.values
        return Matrix2x2([
            [
                a[0][0]*b[0][0] + a[0][1]*b[1][0],
                a[0][0]*b[0][1] + a[0][1]*b[1][1]
            ],
            [
                a[1][0]*b[0][0] + a[1][1]*b[1][0],
                a[1][0]*b[0][1] + a[1][1]*b[1][1]
            ]
        ])

    def __str__(self):
        return f"[{self.values[0]}\n {self.values[1]}]"

Example usage:

m1 = Matrix2x2([[1, 2], [3, 4]])
m2 = Matrix2x2([[5, 6], [7, 8]])
result = m1 @ m2

print(result)
# Output:
# [ [19, 22]
#   [43, 50] ]

This operator overload makes matrix multiplication intuitive and allows your custom matrix class to integrate seamlessly into Python code that uses the @ symbol.

By supporting __matmul__, you open the door to more advanced mathematical operations in your applications.

Final Example: A Complete Money Class with Arithmetic Operators

In this final example, we bring together everything learned about operator overloading by creating a full-featured Money class. This class models real-world money values with a currency and supports all common arithmetic operations using Python’s special methods.

Each arithmetic operator is carefully overloaded to allow natural, readable expressions like adding money amounts, scaling by a number, dividing bills, and more. The class also includes checks to ensure operations are only performed between compatible types and currencies, helping prevent logical errors.

Below is the complete class definition along with example usage that demonstrates the ease and clarity operator overloading brings to working with custom objects.

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

    def __add__(self, other):
        if self.currency != other.currency:
            raise ValueError("Can't add different currencies")
        return Money(self.amount + other.amount, self.currency)

    def __sub__(self, other):
        if self.currency != other.currency:
            raise ValueError("Can't subtract different currencies")
        return Money(self.amount - other.amount, self.currency)

    def __mul__(self, factor):
        if not isinstance(factor, (int, float)):
            raise TypeError("Can only multiply Money by int or float")
        return Money(self.amount * factor, self.currency)

    def __truediv__(self, divisor):
        if not isinstance(divisor, (int, float)):
            raise TypeError("Can only divide Money by int or float")
        return Money(self.amount / divisor, self.currency)

    def __floordiv__(self, divisor):
        if not isinstance(divisor, (int, float)):
            raise TypeError("Can only floor divide Money by int or float")
        return Money(self.amount // divisor, self.currency)

    def __mod__(self, divisor):
        if not isinstance(divisor, (int, float)):
            raise TypeError("Can only modulo Money by int or float")
        return Money(self.amount % divisor, self.currency)

    def __pow__(self, power):
        if not isinstance(power, (int, float)):
            raise TypeError("Power must be int or float")
        return Money(self.amount ** power, self.currency)

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

# Example usage
wallet = Money(150, "USD")
print("Wallet:", wallet)

tip = Money(20, "USD")
total = wallet + tip
print("After tip added:", total)

change = total - Money(50, "USD")
print("After paying $50:", change)

double = change * 2
print("Double the change:", double)

split = total / 3
print("Split total into 3 parts:", split)

split_floor = total // 3
print("Floor split total into 3 parts:", split_floor)

remainder = total % 3
print("Remainder after splitting:", remainder)

power = Money(2, "USD") ** 3
print("Money raised to power 3:", power)

This example shows how operator overloading allows your custom classes to behave intuitively like built-in types, making your code more expressive and easier to read. Feel free to expand this pattern to other domains where natural arithmetic on objects improves clarity and usability.

Conclusion

In this article, we explored how to overload Python’s arithmetic operators to make your custom classes behave like built-in types. Starting with addition (+) and subtraction (-), we saw how to define intuitive operations for classes like Money. Multiplication (*), true division (/), and floor division (//) allowed us to scale values or split amounts, while the modulo operator (%) helped capture remainders in practical ways.

We also covered exponentiation (**), demonstrating element-wise power operations in a Vector2D class, and matrix multiplication (@), showing how to handle 2×2 matrices with natural syntax.

By implementing these magic methods, your classes gain clear, readable, and expressive behavior. They blend seamlessly into Python code, allowing users to write arithmetic expressions without needing to remember special method calls.

Now it’s your turn. Take these examples and experiment with overloading operators in your own domain models. Whether it’s finance, geometry, or something unique to your field, operator overloading can make your classes more powerful and your code more elegant.