Dunder methods (__eq__/__hash__)
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, giving your objects custom behaviour for built-in operators. For instance, defining `__eq__` lets you compare two separate instances by their data rather than their memory identity, and defining `__lt__` (less than) is required for built-in functions like `sorted()` to work. 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. When a dunder method doesn't know how to handle the other operand's type, it must return the singleton (an object with only one instance) `NotImplemented`, which tells Python to try the reverse operation (like `__radd__`) on the other object. Lastly, operators like `+` should return a completely new instance, leaving the original operands unchanged.
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: is vs ==