What is the contract between `__eq__` and `__hash__` in Python, and what breaks when you define only one?
Objects that compare equal must have identical hash values. Python enforces this expectation by setting `__hash__` to `None` whenever you define `__eq__` without `__hash__`, making the object unhashable and ineligible as a dict key or set member. To restore hashability you must define both.
How to think about it
What’s really being tested
Python hash tables (dict, set) do a two-step lookup: hash first, then equality. If two objects compare equal but have different hashes, the lookup fails to find them — the hash points to the wrong bucket. So the language enforces a contract: a == b must imply hash(a) == hash(b).
Python can’t verify that you’ve upheld this contract in your custom class. But it does enforce the simpler half: if you define __eq__, it assumes you’ve changed what “equal” means and it sets __hash__ = None unless you provide one too.
Defining both correctly
The go-to pattern is to delegate __hash__ to a tuple of the fields that define equality. Tuple hashing is well-distributed and order-sensitive — exactly what you want:
Inheriting from a hashable parent
If you subclass a class that already defines __hash__ and you add __eq__, Python still sets __hash__ = None. You must explicitly forward it:
class NamedPoint(Point):
def __init__(self, x, y, label):
super().__init__(x, y)
self.label = label
def __eq__(self, other):
if not isinstance(other, NamedPoint):
return NotImplemented
return super().__eq__(other) and self.label == other.label
__hash__ = Point.__hash__ # opt back in explicitly
Returning NotImplemented
When the comparison involves an incompatible type, return NotImplemented (not False). Python then tries the reflected operation on the other operand, enabling interoperability with third-party types.
The key insight
Hash before equality is an optimization: comparing hashes is a cheap integer comparison that rules out most mismatches before the potentially expensive __eq__ runs. The contract a == b → hash(a) == hash(b) is what makes that optimization safe.