100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Python

Operator Overloading in Python

How to make Python's built-in operators (+, -, *, ==, <, etc.) work on your own classes by implementing the corresponding dunder methods.

OOP AdvancedIntermediate12 min readJul 7, 2026
Analogies

1. Introduction

Operator overloading is the ability to redefine what an operator like +, -, *, ==, or < does when applied to instances of a custom class. Python doesn't let you invent new operator symbols, but it lets you give existing operators new, class-specific meaning by implementing the corresponding dunder method — for example, a + b on custom objects calls a.__add__(b) behind the scenes.

🏏

Cricket analogy: Python won't let you invent a new symbol like a special 'boundary' operator, but it lets + mean something new for a custom 'Score' class -- score_a + score_b calls score_a.__add__(score_b) to combine two innings totals behind the scenes.

This is widely used for mathematical types (vectors, matrices, complex numbers, money), and also for comparison-based types that need custom ordering (e.g. sorting custom priority objects) or custom equality (e.g. comparing domain objects by value rather than identity).

🏏

Cricket analogy: This is like using overloading for a 'RunRate' type doing math, and also for a 'PlayerRanking' type that needs custom ordering by strike rate, or custom equality comparing two players by their stats rather than by which object they are.

2. Syntax

python
class MyNumber:
    def __init__(self, value):
        self.value = value

    def __add__(self, other):
        if not isinstance(other, MyNumber):
            return NotImplemented
        return MyNumber(self.value + other.value)

    def __radd__(self, other):
        # supports other + self when other doesn't know how to add MyNumber
        return self.__add__(other)

    def __lt__(self, other):
        return self.value < other.value

    def __eq__(self, other):
        return isinstance(other, MyNumber) and self.value == other.value

3. Explanation

Each operator maps to a specific dunder method: + -> __add__, - -> __sub__, * -> __mul__, / -> __truediv__, == -> __eq__, != -> __ne__, < -> __lt__, <= -> __le__, > -> __gt__, >= -> __ge__. When you write a + b, Python first tries a.__add__(b); if that returns NotImplemented (e.g. because b's type isn't supported), Python tries b.__radd__(a) — the 'reflected' method — before finally raising TypeError if neither succeeds.

🏏

Cricket analogy: Comparing two batters' averages with > maps to __gt__; if player_a > player_b returns NotImplemented because player_b isn't a comparable type, Python tries player_b.__lt__(player_a) before raising TypeError.

The functools.total_ordering class decorator can reduce boilerplate: define __eq__ and just one of __lt__/__le__/__gt__/__ge__, and it fills in the rest. This is handy for classes that need full ordering support without hand-writing all six comparison dunders.

Gotcha: always return NotImplemented (a special singleton), never raise an exception directly and never return False/None, when an operator method doesn't know how to handle the other operand's type. Returning NotImplemented lets Python try the reflected method on the other object; if you instead return False from __eq__ for an incompatible type, you might get logically wrong results (e.g. a MyNumber accidentally comparing equal to an unrelated object because both a.__eq__ and Python's fallback identity check disagree in confusing ways). If you outright raise, you break Python's protocol for trying the reflected operator.

4. Example

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

    def __repr__(self):
        return f"Vector({self.x}, {self.y})"

    def __add__(self, other):
        if not isinstance(other, Vector):
            return NotImplemented
        return Vector(self.x + other.x, self.y + other.y)

    def __sub__(self, other):
        if not isinstance(other, Vector):
            return NotImplemented
        return Vector(self.x - other.x, self.y - other.y)

    def __mul__(self, scalar):
        if not isinstance(scalar, (int, float)):
            return NotImplemented
        return Vector(self.x * scalar, self.y * scalar)

    __rmul__ = __mul__

    def __eq__(self, other):
        return isinstance(other, Vector) and self.x == other.x and self.y == other.y

    def __lt__(self, other):
        return (self.x ** 2 + self.y ** 2) < (other.x ** 2 + other.y ** 2)


v1 = Vector(2, 3)
v2 = Vector(1, 1)

print(v1 + v2)
print(v1 - v2)
print(v1 * 3)
print(2 * v1)
print(v1 == Vector(2, 3))
print(v2 < v1)

5. Output

text
Vector(3, 4)
Vector(1, 2)
Vector(6, 9)
Vector(4, 6)
True
True

6. Key Takeaways

  • Each Python operator maps to a specific dunder method, e.g. + -> __add__, == -> __eq__, < -> __lt__.
  • Return NotImplemented (not False, None, or an exception) when an operand type isn't supported, so Python can try the reflected method.
  • Reflected methods (__radd__, __rmul__, ...) handle cases where the left operand doesn't know how to combine with your type.
  • functools.total_ordering can auto-generate the remaining comparison methods from __eq__ plus one ordering method.
  • Operator overloading should preserve intuitive semantics — don't make + do something unrelated to addition.
  • Assigning __rmul__ = __mul__ is a common shortcut when the operation is commutative (a * b == b * a).

Practice what you learned

Was this page helpful?

Topics covered

#Python#PythonProgrammingStudyNotes#Programming#OperatorOverloadingInPython#Operator#Overloading#Syntax#Explanation#StudyNotes#SkillVeris