28.3 Reflected Operators: __radd__, __rsub__
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:
- Call
type(x).__add__(x, y) - If step 1 returns
NotImplemented, calltype(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
Avoid Infinite Recursion: The biggest pitfall is accidentally creating infinite recursion within your
__radd__method. A common mistake is to writereturn self + otherinside__radd__, which would call__add__, which might returnNotImplemented, leading back to__radd__again. Always delegate to__add__by calling it directly on the class, as shown in theDistanceexample (self.__add__(other)).Type Checking and
NotImplemented: Your__add__and__radd__methods should always perform robust type checking. If the operation is not defined for the givenothertype, you must returnNotImplemented, not raise an exception. This is the contract of the data model and allows the interpreter to correctly try the reflected method.Asymmetry is Possible: While
__radd__often mirrors__add__, it doesn’t have to. There are valid use cases for asymmetric behavior. For example, aVectorclass 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
- Inheritance from Built-in Types: If your class inherits from a built-in type like
listorint, 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.