Defining __eq__ deletes __hash__¶
Level: 301 · for Python programmers
One line: A class that defines __eq__ and not __hash__ gets __hash__ = None and becomes unhashable, because equal objects must hash equal and Python would rather refuse than let a set hold two copies of one value — so a @dataclass is unhashable unless it is frozen, and an object whose fields change while it sits in a set is lost until they change back.
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
def __eq__(self, other):
return (self.x, self.y) == (other.x, other.y)
Point.__hash__ # None
{Point(1, 2)} # TypeError
A dict and a set find a key by its hash first and confirm it with == second, so the two methods have to agree: whenever a == b, hash(a) must equal hash(b). object keeps that promise trivially, with an __eq__ that means same object and a hash derived from identity. The moment a class says that two different objects are equal, the inherited identity hash breaks the promise, and rather than leave a class in that state the class statement sets __hash__ to None. That is a refusal, not an oversight: the class is unusable as a key until someone says what its hash is.
The other half of the page is what the promise costs once it is kept. A hash computed from fields is only right while the fields hold still, and Python does not stop a hashable object from changing.
The run¶
Verified output of defining_eq_deletes_hash_py.py — regenerated by tools/run_examples.py, never hand-typed.
1. A CLASS WITH NEITHER METHOD: IDENTITY ANSWERS BOTH
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
p, q = Point(1, 2), Point(1, 2)
p == q False no __eq__, so == falls back to is
len({p, q}) 2 and each hashes by identity
2. DEFINE __eq__, AND __hash__ IS GONE
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
def __eq__(self, other):
return (self.x, self.y) == (other.x, other.y)
p, q = Point(1, 2), Point(1, 2)
p == q True
Point.__hash__ None defining __eq__ set __hash__ to None
hash(p) TypeError
{p, q} TypeError unhashable: no set member, no dict key
3. WHY: EQUAL OBJECTS MUST HASH EQUAL
class Bad(Point):
__hash__ = object.__hash__
p, q = Bad(1, 2), Bad(1, 2)
p == q True
len({p, q}) 2 equal, yet two members: the contract is broken
q in {p} False a lookup for q looks where q's hash points
4. THE FIX: HASH THE SAME FIELDS == COMPARES
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
def __eq__(self, other):
return (self.x, self.y) == (other.x, other.y)
def __hash__(self):
return hash((self.x, self.y))
p, q = Point(1, 2), Point(1, 2)
len({p, q}) 1
q in {p} True
{p: 'here'}[q] 'here' q finds p's entry: equal, and hashed alike
5. CHANGE A KEY'S FIELDS, AND THE SET LOSES IT
s = {p}
p.x = 99
p in s False the set looks where the new hash points; p is not there
p in list(s) True a list uses ==, and finds it
p.x = 1
p in s True put the field back, and it is found again
6. WHAT @dataclass DECIDES FOR YOU
@dataclass
class D:
x: int
y: int
D(1, 2) == D(1, 2) True
D.__hash__ None eq=True and not frozen: unhashable, on purpose
@dataclass(frozen=True)
class F:
x: int
y: int
len({F(1, 2), F(1, 2)}) 1 frozen: the fields cannot change, so hashing them is safe
@dataclass(eq=False)
class I:
x: int
I(1) == I(1) False eq=False keeps identity for both
I.__hash__ is object.__hash__ True
7. == IS TWO QUESTIONS, AND NotImplemented PASSES THE FIRST
class Metres:
def __init__(self, v):
self.v = v
def __eq__(self, other):
if not isinstance(other, Metres):
return NotImplemented
return self.v == other.v
Metres(1) == Metres(1) True
Metres(1) != Metres(1) False __ne__ is derived from __eq__
Metres(1) == 1 False NotImplemented from both sides: falls back to is
1 == Metres(1) False int declines first, then Metres is asked
What the run shows¶
Section 1: a class with neither method. Two Point(1, 2) objects are not ==, because object.__eq__ means identity, and len({p, q}) is 2, because each hashes by identity. Consistent, and useless for a value type.
Section 2: define __eq__, and __hash__ is gone. With an __eq__ that compares fields, p == q is True and Point.__hash__ is None. hash(p) raises TypeError, and so does {p, q}: no set member and no dict key. Nothing in the class body mentioned __hash__; the class statement did this on its own.
Section 3: why. Bad puts the identity hash back beside the value __eq__, which is what the class in section 2 would have been without the rule. p == q is True, and len({p, q}) is 2 anyway: two equal members in one set. q in {p} is False, because the lookup computes hash(q), goes where that hash points, and finds nothing there. Equality that the containers cannot see is the state the rule exists to prevent.
Section 4: the fix. A __hash__ built from the same fields __eq__ compares restores the promise: len({p, q}) is 1, q in {p} is True, and {p: 'here'}[q] finds p's entry through q. The idiom is hash((self.x, self.y)), a tuple of exactly the fields that equality uses.
Section 5: change a key's fields, and the set loses it. p is in s; after p.x = 99 its hash is different, the set looks where the new hash points, and p in s is False. p in list(s) is True, because a list compares with == and never asks for a hash, so the object is still in the set's storage. Set p.x back to 1 and p in s is True again. Nothing was removed and nothing was corrupted; the set simply looked in the wrong place, and it will go on looking there for as long as the fields differ. That is why a hashable object should not change, and why the dataclass rules in the next section are what they are.
Section 6: what @dataclass decides for you. With the defaults, eq=True and not frozen, a dataclass gets a field-wise __eq__ and __hash__ = None, which is section 2 done deliberately. frozen=True makes the fields read-only and then hashes them, which is section 4 with section 5 ruled out. eq=False keeps object's identity for both, which is section 1. There is also unsafe_hash=True, which writes a field hash on a class that can still change, and the name says what it is.
Section 7: == is two questions. Metres.__eq__ returns NotImplemented for anything that is not a Metres, and that is not an answer, it is a pass. Python then asks the other operand, and when both sides pass it falls back to identity, so Metres(1) == 1 is False rather than an error, and 1 == Metres(1) is False by the same route with the questions in the other order: int passes first, then Metres is asked. != is derived from __eq__, so Metres(1) != Metres(1) is False without a __ne__ being written. is is not == is that dispatch on its own.
If you are coming from Rust¶
Rust keeps the two as separate derives, PartialEq and Hash, and its containers demand both: a HashSet<T> needs T: Eq + Hash, so a type with neither is refused at compile time where Python refuses at run time in section 2. Nothing in the compiler checks that a hand-written PartialEq agrees with a derived Hash, but the documentation states the same promise and clippy warns on that exact combination, which is the section 3 mistake caught by a lint. Section 5 cannot be written: a value inside a HashSet cannot be mutated through the set. The Rust library's Comparison traits ↗ and The HashSet ↗ are the two sides.
If you are coming from C¶
C has no operators to overload and no hash table in the language. A table you write takes an equality function and a hash function as a pair, and keeping them consistent is your job and nobody checks. Python's rule in section 2 is the language doing that job for you, by removing the half of the pair that stopped matching.
If you are coming from ABAP¶
A hashed or sorted internal table declares its key columns, and both "equal" and "hash" are those columns by construction, so there is no way to define one without the other and the section 3 state cannot arise. ABAP also forbids section 5 outright: the key fields of a row in a hashed table cannot be changed in place, and a MODIFY that tries is an error. Python allows the change and loses the object instead, which is the trade this page is about. (Not machine-checked — CI cannot run ABAP.)
Try it¶
- Search a codebase you maintain for
def __eq__and, for each class, check whetherdef __hash__is beside it and whether instances are ever put in a set, used as dict keys, or passed to a function underfunctools.lru_cache, which hashes its arguments. - Find a
@dataclassthat is used as a dict key. If it is notfrozen=True, work out how it ever worked. - Put an object of yours in a set, change one of the fields its
__eq__compares, and test membership, then change the field back. Section 5 says it comes and goes. - Give a class of yours an
__eq__that returnsNotImplementedfor foreign types, and compare an instance againstNone, against anintand against an instance of an unrelated class. None of the three should raise. - Sort a list of your objects with
sorted(). If it raisesTypeError, your class has__eq__and no ordering, which is the same design decision one operator over.
See also¶
isis not==: section 7's dispatch, and the identity fallbackhash()is not stable across runs: the other half of the contract, from the built-in types' side- Comparing an
intwith afloat: section 6 there, where equal numbers of five types are one dict key because the promise holds across types @dataclasswrites the methods: the rest of what section 6's decorator decidesobject.__hash__↗ in the language reference, whose paragraphs state the rule in section 2, and thedataclass↗ parameterseq,frozenandunsafe_hash- Comparison traits ↗ and The
HashSet↗ in the Rust library