skip to content

__eq__ and __hash__ Contract

Defining __eq__ silently sets __hash__ to None, so your objects stop working as dict keys, and equal objects must hash equal. The most-asked object-model contract in Python interviews.

part ofPythonoverview, primer and where to startread it →
on this pageshow

questions

4

What contract must `__hash__` satisfy relative to `__eq__` in Python?

level: middleimportance: must knowfreq 64%

answer

  1. The rule runs one way only
  2. Collisions are legal, mismatches are not
  3. The hash chooses where; equality chooses which
  4. A second clause covers time, not pairs
  5. Same field tuple in both methods

basics

~10 s

Objects that compare equal must return equal hash values, and an object's hash must not change while it lives. The converse is not required: equal hashes are a collision, resolved by ==.

solid answer

~40 s

The rule runs in one direction only: **if `a == b` then `hash(a) == hash(b)`**. The converse is not required — two unequal objects may share a hash, that is a collision, and `==` settles it. The contrapositive is the useful form: different hashes mean the container will never even try `==`, so an object with a "wrong" hash is simply invisible where it should be found. A second clause is just as important: the hash must stay constant for the object's lifetime, or at least while it sits in any hash-based container. Python enforces none of this — it only nulls `__hash__` when you define `__eq__`; the rest is on you. The practical recipe is one tuple of the value-defining, non-mutating fields, passed to `hash()` in `__hash__` and compared in `__eq__`.

code

python · 16 lines
python
class Case:
    def __init__(self, case_id):
        self.case_id = case_id

    def __eq__(self, other):
        if not isinstance(other, Case):
            return NotImplemented
        return self.case_id == other.case_id

    def __hash__(self):
        return id(self)          # equal objects hash differently


seen = {Case(7)}
print(Case(7) == Case(7))   # True
print(Case(7) in seen)      # False - the lookup never reaches __eq__

go deeper

for a junior

Memorise the one-way rule: equal objects must have equal hashes, but equal hashes do not mean the objects are equal. Know that a set uses both, in that order.

for a middle

Explain why the direction matters — the hash chooses where the container looks, so a mismatched hash means == is never called — and add the stability clause about the value not changing over the object's life.

for a senior

Volunteer the operational consequences: collisions are a performance concern rather than a correctness one, str hashes are salted per process so they cannot be persisted, and Python verifies none of the contract for you.

for a principal

Decide how the codebase makes the contract structural rather than remembered — immutable value types, a single key-tuple convention, and review rules — so correctness does not depend on each author recalling both clauses.

## The contract, stated exactly Two clauses: 1. **Consistency with equality.** If `a == b` is true, then `hash(a) == hash(b)` must be true. 2. **Stability.** `hash(a)` must return the same value every time it is called, for as long as the object exists — strictly, for as long as it is a member of any hash-based container. Everything else people believe about hashing is not in the contract. Hashes need not be unique. Unequal objects are perfectly entitled to share a hash. `hash()` need not be expensive, cryptographic, or well distributed to be *correct* — only to be *fast*. ## Why the direction matters A hash-based container uses the hash to choose **where** to store and where to look; only among the entries it finds there does it use `==` to decide which one you meant. That is the whole reason for the asymmetry: - **Equal but different hashes** is a correctness bug. The lookup goes to the wrong place, `==` is never called, and the object is unfindable even though it is stored. - **Unequal but equal hashes** is a collision. The container finds both candidates in the same place and `==` rejects the wrong one. Nothing breaks; you have merely spent a little more time. The contrapositive is the sentence to say in an interview: *different hashes assert "definitely not equal", so `==` is never given the chance to disagree.* A hash function is an index, and `__eq__` is the judge. ## Correct but terrible `def __hash__(self): return 0` satisfies the contract perfectly — every equal pair trivially hashes equal. It also turns every `dict` and `set` of those objects into a linear scan, because every key lands in the same place and `==` must be run against all of them. Correctness and performance are separate axes here: the contract constrains correctness, and distribution buys speed. Hashing a tuple of the value fields gets you both, because `tuple.__hash__` mixes its elements' hashes for you. ## Stability, and where it bites The second clause is the one people forget, and it is the reason mutability and hashing are in tension. If `__hash__` reads a field that can change, then an object inserted before the change and looked up after it produces two different hashes, and clause 2 is broken even though clause 1 was satisfied at every instant. Python's own answer to this is visible in the builtins: `str`, `int`, `frozenset` and `tuple` are hashable; `list`, `dict` and `set` are not. Follow the same discipline — hash only fields that never change after construction, or accept that the type does not belong in a hash-based container at all. ## What Python does and does not check Python does exactly one thing for you: a class body that defines `__eq__` without `__hash__` gets `__hash__` set to `None`, so the mismatch cannot be inherited silently. Beyond that, nothing is verified. Nothing checks that your `__eq__` is reflexive, symmetric or transitive; nothing checks that your `__hash__` agrees with it; nothing notices when you mutate a hashed field. Asymmetric equality across a class hierarchy — where `parent == child` is `True` but `child == parent` is `False` because the subclass compares an extra field — produces container results that depend on which object was inserted first, and no tool will warn you. ## Hash values are not identifiers Two practical corollaries a senior candidate should volunteer. First, **hash values are not stable across processes**. Python salts the hashes of `str` and `bytes` per interpreter run as a defence against algorithmic-complexity attacks on hash-based containers; the salt is controlled by the `PYTHONHASHSEED` environment variable, and randomization is on by default. So never persist `hash(some_string)` to a database, a cache key file, or a shard assignment that must survive a restart. When you need a stable digest, use `hashlib`, whose values are defined by the algorithm rather than by the run. Second, **`hash()` is not a fingerprint of contents**. It is a small integer with a huge collision space by design; two very different objects sharing a hash is normal, so `hash(a) == hash(b)` never proves equality. (A CPython implementation detail on the same theme: a computed hash of `-1` is reported as `-2`, because `-1` is reserved to signal an error at the C level — which is why `hash(-1)` is `-2`.) ## The recipe Build one tuple of the fields that define the value, and use it in both places — compared in `__eq__`, hashed in `__hash__`. Freeze those fields after construction. If you cannot freeze them, do not make the type hashable. That single habit satisfies both clauses by construction and removes the entire class of bug from the codebase.

  • Is `return 0` a valid `__hash__` implementation?
    It is valid but pathological. Every equal pair hashes equal, so the contract holds, and a `set` still de-duplicates correctly. But every key lands in the same place, so each lookup degrades to comparing against every stored key with `==`. The contract governs correctness; distribution governs performance. Hashing a tuple of the value fields gives you both, since tuples mix their elements' hashes.
  • Can two unequal objects share a hash value, and what happens if they do?
    Yes — that is a collision, and it is expected. Hash values are small integers over an unbounded space of objects, so collisions are unavoidable. The container finds both candidates and calls `==` to discriminate, so results stay correct; only lookup cost rises. What is not allowed is the reverse: equal objects with different hashes, which makes a stored key unreachable.
  • Why should you never store `hash(some_string)` in a database?
    String and bytes hashes are salted per interpreter process by default, as a defence against deliberately colliding inputs; the salt is governed by `PYTHONHASHSEED`. The same string therefore hashes to different values in different runs, so any persisted value, shard assignment or cache key built from it breaks after a restart. Use `hashlib` when you need a digest that is stable across processes and machines.

saying these in an interview costs you the question

  • Says equal hashes imply equal objects
  • Claims hash values must be unique
  • Ignores that the hash must stay constant over time
  • Hashes a field the `__eq__` does not compare
  • Persists a `str` hash across process restarts
  • Assumes Python verifies the contract for you

context

open as a page

Why does defining `__eq__` make a class's instances unusable as dict keys?

level: middleimportance: must knowfreq 72%

basics

~10 s

When a class body defines eq without hash, Python sets hash to None in that class, so hash() raises TypeError: unhashable type. Define hash over the same fields to restore it.

open as a page

With no `__eq__` defined, how does `==` behave on class instances?

level: juniorimportance: should knowfreq 58%

basics

~10 s

A class with no eq inherits object.eq, which compares identity: x == y is True only when both names refer to the very same object. Two instances built from identical data are still unequal.

open as a page

Why does mutating an object already used as a `dict` key make later lookups miss it?

level: seniorimportance: should knowfreq 46%

basics

~20 s

The container placed the key using the hash it had at insertion. Mutating a hashed field changes the hash, so lookups search the wrong place: the key tests as absent while iteration still yields it.

open as a page