When Python encounters an expression like x + y, it first attempts to call x.__add__(y). If x’s class does not implement __add__, or if the method returns the special NotImplemented value, Python does not immediately give up. Instead, it checks if y’s class implements the corresponding “reflected” or “right-hand” method, in this case, __radd__ (right-add). This fallback mechanism is a cornerstone of the Python data model, designed to allow for flexible and intuitive operations between objects of different types, even when one of them wasn’t originally designed to work with the other.

The __radd__ method is invoked with one argument: the left-hand operand. Its signature is def __radd__(self, other). The same logic applies to all reflected arithmetic operators: __rsub__, __rmul__, __rtruediv__, etc.

The Lookup Mechanism and NotImplemented

Understanding the precise order of operations is critical. The sequence for x + y is:

  1. Call type(x).__add__(x, y)
  2. If step 1 returns NotImplemented, call type(y).__radd__(y, x)

The NotImplemented singleton is not the same as the NotImplementedError exception. Returning NotImplemented is a deliberate signal to the interpreter that the operation is not supported for the given type combination, triggering the fallback to the reflected method. Raising NotImplementedError would break the entire operation.

class LeftSide:
    def __add__(self, other):
        print(f"LeftSide.__add__ called with {type(other)}")
        # We don't know how to add a RightSide, so signal to try __radd__
        return NotImplemented

class RightSide:
    def __radd__(self, other):
        print(f"RightSide.__radd__ called with {type(other)}")
        return f"Result of adding {other} and a RightSide"

left = LeftSide()
right = RightSide()
result = left + right  # This calls LeftSide.__add__, then RightSide.__radd__
print(result)

Output:

LeftSide.__add__ called with <class '__main__.RightSide'>
RightSide.__radd__ called with <class '__main__.LeftSide'>
Result of adding <__main__.LeftSide object at 0x...> and a RightSide

Practical Use Case: Commutative Operations

The most common use for reflected operators is to implement commutative operations where the order shouldn’t matter, such as addition. A prime example is when you want your custom object to be able to be added to a built-in type.

class Distance:
    def __init__(self, value):
        self.value = value

    def __add__(self, other):
        # If other is also a Distance, add their values
        if isinstance(other, Distance):
            return Distance(self.value + other.value)
        # If other is a number, treat it as meters
        elif isinstance(other, (int, float)):
            return Distance(self.value + other)
        else:
            return NotImplemented

    def __radd__(self, other):
        # This allows for (int + Distance) by delegating to __add__
        # e.g., 5 + Distance(10) becomes Distance(10).__radd__(5) -> Distance(15)
        return self.__add__(other)

    def __repr__(self):
        return f"Distance({self.value} m)"

# Usage
d1 = Distance(5)
d2 = Distance(10)

print(d1 + d2)        # Works via __add__
print(d1 + 15)        # Works via __add__
print(15 + d1)        # Works via __radd__

Output:

Distance(15 m)
Distance(20 m)
Distance(20 m)

Common Pitfalls and Best Practices

  1. Avoid Infinite Recursion: The biggest pitfall is accidentally creating infinite recursion within your __radd__ method. A common mistake is to write return self + other inside __radd__, which would call __add__, which might return NotImplemented, leading back to __radd__ again. Always delegate to __add__ by calling it directly on the class, as shown in the Distance example (self.__add__(other)).

  2. Type Checking and NotImplemented: Your __add__ and __radd__ methods should always perform robust type checking. If the operation is not defined for the given other type, you must return NotImplemented, not raise an exception. This is the contract of the data model and allows the interpreter to correctly try the reflected method.

  3. Asymmetry is Possible: While __radd__ often mirrors __add__, it doesn’t have to. There are valid use cases for asymmetric behavior. For example, a Vector class might define __mul__ for dot products with other vectors and __rmul__ for scalar multiplication with integers/floats. The operations are fundamentally different.

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

    def __mul__(self, other):
        # Define multiplication with another Vector as dot product
        if isinstance(other, Vector):
            return self.x * other.x + self.y * other.y
        else:
            return NotImplemented

    def __rmul__(self, other):
        # Define multiplication with a scalar (on the left) as scaling
        if isinstance(other, (int, float)):
            return Vector(other * self.x, other * self.y)
        else:
            return NotImplemented

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

v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 * v2)   # Dot product: 1*3 + 2*4 = 11
print(3 * v1)    # Scalar multiplication: Vector(3, 6)
# v1 * 3 would raise a TypeError because __mul__ returns NotImplemented and int.__rmul__ doesn't know about Vector
  1. Inheritance from Built-in Types: If your class inherits from a built-in type like list or int, you often get the reflected operator methods for free. However, you must be cautious, as the built-in implementation might not be compatible with your custom class’s intended behavior. Always test these interactions thoroughly.