Skip to content

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

  1. Search a codebase you maintain for def __eq__ and, for each class, check whether def __hash__ is beside it and whether instances are ever put in a set, used as dict keys, or passed to a function under functools.lru_cache, which hashes its arguments.
  2. Find a @dataclass that is used as a dict key. If it is not frozen=True, work out how it ever worked.
  3. 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.
  4. Give a class of yours an __eq__ that returns NotImplemented for foreign types, and compare an instance against None, against an int and against an instance of an unrelated class. None of the three should raise.
  5. Sort a list of your objects with sorted(). If it raises TypeError, your class has __eq__ and no ordering, which is the same design decision one operator over.

See also