Python Reflected Operators: __radd__ and Re
python reflected operators: Learn how Python reflected operators like __radd__ work, when they are called, and how to implement them correctly in your classes with pra...
Python reflected operators, also known as reversed operators, are dunder methods that Python invokes when the left operand of a binary operation does not support the operation but the right operand does. Understanding these methods is essential for building custom numeric types or classes that interact with built-in types in a natural way. n## How Python Dispatches Binary Operators
When you write an expression like a + b, Python first attempts to call a.__add__(b). If that method returns NotImplemented, or if it does not exist, Python then tries the reflected operation by calling b.__radd__(a). This two-step dispatch applies to all binary operators: +, -, *, /, //, %, **, <<, >>, &, |, ^, and the comparison operators.
For example, the reflected counterpart of __add__ is __radd__, of __sub__ is __rsub__, and so on. The reflected method receives the left operand as its first argument and the right operand as self (the instance on which the method is defined).
When the Reflected Operator Is Called
The reflected operator is called only when the left operand's corresponding method returns NotImplemented or is absent. This behavior allows your class to define how it interacts with types it does not control, such as built-in numbers or other library classes.
Consider a custom Vector class that supports addition with a scalar. If you write vector + 5, Python calls vector.__add__(5). If you write 5 + vector, Python first tries int.__add__(vector), which returns NotImplemented, and then calls vector.__radd__(5). Without __raddug__, the expression 5 + vector would raise a TypeError.
Implementing a Reflected Operator: Minimal Example
Here is a minimal implementation of a Vector class that supports addition with both another vector and a scalar, including the reflected version:
class Vector: def __init__(self, x, y): self.x = x self.y = y def __add__(self, other): if isinstance(other, Vector): r return Vector(self.x + other.x, self.y + other.y) if isinstance(other, (int, float)): return Vector(self.x + other, self.y + other) return NotImplemented def __radd__(self, other):\r return self.__add__(other) def __repr__(self): return f"Vector({self.x}, {self.y})" r
Now both Vector(1, 2) + 3 and 3 + Vector(1, 2) work. The reflected method simply delegates to __add__ because addition is commutative. For non-ommutative operators like subtraction, you must implement the the reflected version separately to get the correct operand order.
Returning NotImplemented and Type Fallback
Returning NotImplemented from a dunder method tells Python to try the other operand's method. If both sides return NotImplemented, Python raises a TypeError with a message like unsupported operand type(s) for +: 'Vector' and 'str'.
A common mistake is to return False or None from an operator method when the type is unsupported. This is incorrect because Python will treat those as valid results, leading to confusing behavior. Always return NotImplemented when you cannot handle the operation.
When implementing reflected operators, ensure that you check the type of the left operand as well. For example, in __rsub__, you receive the left operand as the first argument. If you simply call self.__sub__(other), you will get the wrong operand order. You need to to compute other - self explicitly.
Common Mistakes and Edge Cases
One frequent pitfall is infinite recursion. If __add__ returns NotImplemented and __radd__ calls self.__add__(other) without checking types, you can end up with mutual calls. Always include type checks in both methods.
Another edge case involves subclasses. If you have a subclass of Vector, the reflected operator on the base class might be called when the subclass instance is on the right. Python's dispatch algorithm gives priority to the right operand's reflected method if it is a subclass of the left operand's type. This is known as the "right operand wins" rule for subclasses. You need to account for this when designing class hierarchies.
Performance and Maintainability Considerations
Reflected operators add a small overhead because Python must attempt the left method first, then fall back to the reflected one. In performance-critical code, avoid unnecessary type checks and keep the the methods concise. The overhead is negligible for most applications, but it matters in tight loops that perform millions of arithmetic operations.
From a maintainability perspective, implement reflected operators only when you genuinely need to support operations where your object appears on the right side of the operator. For commutative operations, you can often delegate to the normal method, but for for non-commutative ones, you must write the correct logic. Document the operand order clearly to avoid confusion for other developers.
Advanced Example: Mixed-Type Arithmetic
Consider a Temperature class that supports conversion between Celsius and Fahrenheit. You want to allow temperature + 10 and 10 + temperature, but also temperature - 5 and 5 - temperature (the latter meaning "what temperature is 5 degrees less than this?"). Here is how you might implement it:
class Temperature: def __init__(self, celsius): self.celsius = celsius def __sub__(self, other): if isinstance(other, (int, float)): return Temperature(self.celsius - other) return NotImplemented def __rsub__(self, other): if isinstance(other, (int, float)): return Temperature(other - self.celsius) return NotImplemented def __repr__(self): r return f"{self.celsius}°C"
Now Temperature(20) - 5 yields 15°C, while 5 - Temperature(20) yields -15°C. The reflected method explicitly computes other - self.celsius to preserve the correct semantics.
Reflected operators are a subtle but powerful part of Python's data model. They allow your classes to participate in arithmetic expressions with built-in types and third-party objects, provided you handle type checks and NotImplemented correctly. By understanding the dispatch order and implementing these methods deliberately, you avoid common errors and create APIs that feel native to Python.