Operator Overloading: __add__, __eq__, __lt__
Overview
In Python, operators like `+`, `==`, and `<` are syntactic sugar for method calls on objects. When you run `a + b`, Python translates it to `a.__add__(b)`. Operator overloading allows you to define these double-underscore (dunder) methods in your own classes. class Point: def __init__(self, x, y): self.x = x self.y = y def __add__(self, other): return Point(self.x + other.x, self.y + other.y) Think of operators as universal remotes: the button `+` is standard, but the specific device (the object) decides what action is taken when the button is pressed. This integration lets custom objects work seamlessly with built-in functions like `sorted()`, which requires `__lt__` (less than) to be defined. To implement operator overloading properly, you must follow specific rules for each method: - `__eq__(self, other)`: Compares two objects for equality. Returns a boolean, or `NotImplemented` if the type is unknown. It does not raise exceptions on type mismatch. - `__lt__(self, other)`: Compares if `self` is less than `other`. Returns a boolean, or `NotImplemented`. Python raises a `TypeError` if the fallback fails. - `__add__(self, other)`: Adds two objects. Returns a completely new instance of the object. Python raises a `TypeError` if neither operand supports the addition. When a dunder method doesn't recognize the other operand's type, it must return the singleton `NotImplemented`. This acts as a signal for Python to try the reverse operation (like `__radd__` or `__req__`) on the right-hand operand, enabling seamless interoperability between different types. # Trap: Raising an error prevents the fallback mechanism def __add__(self, other): if not isinstance(other, Point): raise TypeError("Expected Point") return Point(self.x + other.x, self.y + other.y) Raising an error instead of returning `NotImplemented` is a common edge case that breaks Python's fallback mechanism. If your method hard-fails, the right-hand operand never gets a chance to resolve the operation using its own dunder methods.
It allows custom objects to integrate seamlessly with Python's built-in syntax and functions, making code cleaner and more expressive. It also enables objects to be sorted or compared for equality without writing custom method names.
Where used: Pandas, NumPy, Pathlib
Why learn this
- Customizing equality logic for comparing objects in tests
- Using `sorted()` or `max()` on lists of custom models
- Writing intuitive vector or point math objects
Code walkthrough
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Point(self.x + other.x, self.y + other.y)
p1 = Point(1, 2)
p2 = Point(3, 4)
p3 = p1 + p2
print(p3.x, p3.y)
Focus: p3 = p1 + p2
Aha moment
class Box:
def __init__(self, value):
self.value = value
b1 = Box(10)
b2 = Box(10)
print(b1 == b2)
Prediction: What does this print?
Common guess: `True`
By default, custom classes use object identity for equality (like `is`), so two separate instances evaluate to `False`. You must explicitly implement `__eq__` to compare their values.
Common mistakes
- Returning `NotImplemented` vs `NotImplementedError`: When an operator method doesn't know how to handle the other operand's type, it should return the singleton `NotImplemented`, not raise `NotImplementedError`. Returning `NotImplemented` signals Python to try the reverse operation on the other object.
- Mutating `self` instead of returning a new object: For operations like `+`, beginners sometimes mutate `self` and return it. Operators like `+` should return a completely new instance, leaving the original operands unchanged.
Glossary
- syntactic sugar
- Syntax in a programming language that makes things easier to read or to express, but doesn't add any new functionality. Example: `a + b`.
- operand
- The object or value that an operator (like + or -) works on or manipulates. Example: `3 + 5`.
Recall questions
- What happens internally when you use the `+` operator on two objects?
- If you want to allow instances of a custom class to be sorted using the built-in `sorted()` function, which dunder method must you implement?
- What should an operator method return if it does not know how to handle the type of the other operand?
Understanding checks
What is the output of this script?
15, 15
The `__add__` method mutates `self` instead of creating and returning a completely new instance. Thus, both `p1` and `p3` refer to the same object, and `p1` has been permanently modified by the addition.
When defining the `__eq__` method, what is the correct way to handle comparison with an unsupported type?
Return the singleton `NotImplemented`. This tells Python to try the reverse operation on the other object before falling back to default behavior.
Returning `NotImplemented` tells Python that this method does not know how to handle the other operand's type. Python will then attempt to call the reverse operation (e.g., `__req__` or the other object's `__eq__`) before finally falling back to default behavior.
Practice tasks
Refactor User Equality
The `User` class currently uses the default equality behavior (comparing memory identity). Modify the `__eq__` method so that two `User` instances are considered equal (`==`) if they have the same `user_id`. Also ensure that comparing a `User` with a non-`User` object returns `NotImplemented` instead of raising an error or returning `False`.
Challenge
Vector Mathematics
Write a `Vector` class that initializes with `x` and `y` components. Implement `__add__` to add two vectors together returning a new `Vector`. Also implement `__eq__` so that two vectors are equal if their `x` and `y` components are identical. Ensure that if `__add__` or `__eq__` is used with a non-`Vector` object, it returns `NotImplemented`.
Continue learning
Previous: Everything is an object in Python
Previous: is vs == : Identity vs Equality