Operator Overloading in Python

· 4 min read · Updated March 14, 2026 · intermediate
operators oop classes dunder-methods patterns

Operator overloading lets your custom objects work with Python’s built-in operators. When you use + between two objects, Python calls the first object’s add method. When you compare objects with ==, Python calls eq. By defining these special methods, you control how operators behave with your types.

How Operator Overloading Works

Python operators are syntactic sugar for method calls. When you write a + b, Python essentially runs a.add(b). If that method returns NotImplemented, Python tries the reflected operation on the right operand: b.radd(a).

This design lets you define meaningful behavior for operators without changing how the language works:

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)
    
    def __repr__(self):
        return f"Vector({self.x}, {self.y})"

v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2)  # Vector(4, 6)

Arithmetic Operators

Python defines methods for all arithmetic operators:

OperatorMethodDescription
+addAddition
-subSubtraction
*mulMultiplication
/truedivDivision
//floordivFloor division
%modModulo
**powExponentiation
@matmulMatrix multiplication

Here’s a number class that handles multiplication:

class Money:
    def __init__(self, amount):
        self.amount = amount
    
    def __mul__(self, multiplier):
        return Money(self.amount * multiplier)
    
    def __rmul__(self, multiplier):
        return Money(self.amount * multiplier)
    
    def __repr__(self):
        return f"${self.amount}"

price = Money(50)
print(price * 3)    # $150
print(3 * price)    # $150 — uses __rmul__

The reflected rmul method handles cases where the left operand doesn’t support the operation.

Comparison Operators

Comparison operators let your objects work with ==, !=, <, >, <=, >=:

class Temperature:
    def __init__(self, celsius):
        self.celsius = celsius
    
    def __eq__(self, other):
        return self.celsius == other.celsius
    
    def __lt__(self, other):
        return self.celsius < other.celsius
    
    def __le__(self, other):
        return self == other or self < other
    
    def __gt__(self, other):
        return not self <= other
    
    def __ge__(self, other):
        return not self < other

t1 = Temperature(20)
t2 = Temperature(25)
print(t1 < t2)   # True
print(t1 == Temperature(20))  # True

Python automatically derives != from eq (and > from <=).

Boolean and Truthiness

Define how your object behaves in boolean contexts with bool:

class EmptyBuffer:
    def __init__(self, data=None):
        self.data = data or []
    
    def __bool__(self):
        return len(self.data) > 0

buffer = EmptyBuffer()
print(bool(buffer))  # False

buffer.data = [1, 2, 3]
print(bool(buffer))  # True

If you don’t define bool, Python falls back to len (truthy if non-zero).

Common Pitfalls

Returning the wrong type: Methods should return new objects, not modify existing ones:

# Wrong
def __add__(self, other):
    self.x += other.x  # Modifies self!
    return self

# Correct
def __add__(self, other):
    return Vector(self.x + other.x, self.y + other.y)

Forgetting NotImplemented: When you can’t handle an operation, return NotImplemented (not NotImplementedError):

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

This lets Python try the reflected operation on the other object.

Bitwise Operators

You can also overload bitwise operators:

OperatorMethodDescription
&andBitwise AND
or
^xorBitwise XOR
~invertBitwise NOT
<<lshiftLeft shift
>>rshiftRight shift
class Permission:
    def __init__(self, bits):
        self.bits = bits
    
    def __or__(self, other):
        return Permission(self.bits | other.bits)
    
    def __and__(self, other):
        return Permission(self.bits & other.bits)
    
    def __bool__(self):
        return self.bits > 0
    
    def __repr__(self):
        perms = []
        if self.bits & 1: perms.append("read")
        if self.bits & 2: perms.append("write")
        if self.bits & 4: perms.append("execute")
        return f"Permission({', '.join(perms) or 'none'})"

read = Permission(1)
write = Permission(2)
execute = Permission(4)

print(read | write)    # Permission(read, write)
print(read & write)    # Permission(none)

When to Use Operator Overloading

Operator overloading shines when building domain-specific types that represent mathematical or logical concepts:

  • Numeric types: Vector, Matrix, Complex, Fraction, Money
  • Collections with ordering: Range, Interval, Version
  • Set-like types: Bag, Multiset, BitSet
  • Physical quantities: Distance, Temperature, Time

Avoid overloading operators when the meaning would be confusing.

Summary

Operator overloading makes your classes feel native to Python. Define special methods like add, eq, lt, and bool to control how operators behave with your objects. Return NotImplemented when you can’t handle an operation, and always return new objects rather than mutating existing ones.

See Also