skip to content

Which @dataclass flags decide whether its instances are hashable?

level: middleimportance: must knowfreq 55%

answer

  1. Two flags decide it, not one
  2. Defining equality costs you the inherited hash
  3. Immutability is what buys the hash back
  4. eq=False keeps the identity hash
  5. unsafe_hash trades away hash stability

basics

~10 s

With the defaults (eq=True, frozen=False) the decorator sets hash to None, so instances are unhashable. eq=True together with frozen=True generates a hash; eq=False leaves the inherited identity-based hash alone; unsafe_hash=True forces one.

solid answer

~40 s

Three rules cover it. With `eq=True` and `frozen=False` — the defaults — `@dataclass` sets `__hash__ = None`, so instances cannot go in a `set` or be dict keys; that mirrors Python's general rule that defining `__eq__` drops the inherited hash. With `eq=True` and `frozen=True` the decorator generates a `__hash__` that hashes the tuple of the fields used for comparison, so equal instances hash equal. With `eq=False` the decorator touches neither, so the class keeps `object.__hash__` and hashes by identity — two equal-looking instances hash differently. `unsafe_hash=True` forces a generated hash onto a mutable dataclass; it is called unsafe because mutating a compared field changes the hash, and the object is then lost inside any set or dict that already holds it.

code

python · 18 lines
python
from dataclasses import dataclass

@dataclass
class Mutable:
    x: int

@dataclass(frozen=True)
class Frozen:
    x: int

@dataclass(eq=False)
class NoEq:
    x: int

print(Mutable.__hash__)                      # None -> unhashable
print(hash(Frozen(1)) == hash(Frozen(1)))    # True  -> value hash
print(hash(NoEq(1)) == hash(NoEq(1)))        # False -> identity hash
print(hash(Frozen(1)) == hash((1,)))         # True  -> hashes the field tuple

go deeper

for a junior

Recall the symptom: putting a plain dataclass instance in a set raises TypeError: unhashable type. Know that adding frozen=True is the usual fix.

for a middle

Explain all three combinations of eq and frozen and what each does to hash, and connect it to Python's rule that defining eq blanks the inherited hash.

for a senior

Show why the hash contract needs stability, not just equality, and be able to describe the concrete corruption unsafe_hash invites when a compared field is mutated after insertion into a set.

for a principal

Own the modelling call: which types in the codebase are values that may be keys and cached, and which are entities with identity. That decision, not the flag, is what makes eq/frozen fall out obviously.

## The rule set Hashability of a dataclass is decided entirely by the `eq`, `frozen` and `unsafe_hash` arguments to `@dataclass`, and the rules are short enough to memorise: | `eq` | `frozen` | result | |---|---|---| | `True` (default) | `False` (default) | `__hash__` is set to `None` — instances are **unhashable** | | `True` | `True` | a `__hash__` is **generated** from the comparison fields | | `False` | either | `__hash__` is left untouched — the class inherits `object.__hash__` and hashes **by identity** | `unsafe_hash=True` overrides the first row and forces generation. ## Why the default is unhashable This is not a dataclass invention; it is Python's general object model. Any class that defines `__eq__` and does not define `__hash__` gets `__hash__ = None` from the type machinery. The reason is the hash invariant: **objects that compare equal must hash equal**, and the hash of an object must not change while it lives in a hash table. If a class redefines equality in terms of its data, an identity-based inherited hash would break the first half of that contract — two equal instances would land in different buckets, and `x in some_set` would return `False` for an object equal to a member. Since `@dataclass` generates an `__eq__` by default, it follows the same rule and blanks the hash. The error you get is `TypeError: unhashable type: 'Reading'` the first time someone puts a record in a set. ## Why frozen fixes it The *second* half of the contract is that the hash must be stable. A frozen dataclass refuses attribute assignment, so the compared fields cannot be rebound after construction, and the decorator is willing to generate a hash. The generated implementation hashes the tuple of the fields that participate in comparison — for a class with a single field `x`, `hash(obj)` is literally `hash((obj.x,))`. That gives exactly the semantics you want for value objects: equal records hash equal, so they deduplicate in a `set` and collide correctly as dict keys. Note the subtlety that frozen is shallow. If a field holds a list, the field type is unhashable and `hash(obj)` raises `TypeError` at call time even though the class is nominally hashable. Value objects intended as keys should hold immutable field types — `str`, `int`, `tuple`, `frozenset`, other frozen dataclasses. ## The eq=False case is the one people get backwards It is tempting to read `eq=False` as "less capable, therefore less hashable". The opposite is true: with `eq=False` the decorator generates no `__eq__`, the inherited `object.__eq__` and `object.__hash__` survive, and instances are hashable by identity. They compare equal only to themselves. That is occasionally exactly what you want — a mutable node object you need to put in a set while still using its data-carrying `__repr__` — but it is a different semantic, and mixing it up produces sets that never deduplicate. ## unsafe_hash and when it is defensible `unsafe_hash=True` generates the same field-based hash on a class you can still mutate. The name is a warning, not a scare: it means *you* are now responsible for the stability half of the contract. The failure it invites is concrete — put an instance in a set, mutate a compared field, and the object is unreachable in that set even though the set still holds it: ```python from dataclasses import dataclass @dataclass(unsafe_hash=True) class Point: x: int p = Point(1) seen = {p} p.x = 99 print(p in seen) # False, and the set is now corrupt for lookups ``` Defensible uses are narrow: a record that is mutated only during construction and treated as frozen afterwards, or a legacy class where making it frozen would break too many callers at once. The honest default is `frozen=True`, and reaching for `unsafe_hash` is a signal the class is trying to be a value and a mutable entity at the same time. ## Equality across classes One more thing worth carrying into the interview: the generated `__eq__` compares the tuple of fields **only when both operands are of the same class**. Two structurally identical dataclasses with the same field values are never equal, and they will not deduplicate against each other in a set. Equality returns `NotImplemented` for the foreign type, and Python falls back to identity comparison, which is `False`. Finally, hashability is inherited like any other attribute: a subclass of an unhashable dataclass is still unhashable unless it re-enables hashing, and a plain (non-dataclass) subclass that defines its own `__eq__` restarts the whole rule from the top.

  • Why does defining __eq__ on any Python class remove the inherited __hash__?
    Because the two must agree: objects that compare equal are required to hash equal. Redefining equality in terms of data while keeping the identity-based `object.__hash__` would break that, so the type machinery sets `__hash__ = None` unless the class supplies its own. `@dataclass` simply follows the same rule, which is why the default dataclass is unhashable.
  • What exactly does the generated __hash__ of a frozen dataclass hash?
    The tuple of the fields that take part in comparison, in declaration order — for a one-field class, `hash(obj)` equals `hash((obj.x,))`. Fields excluded from comparison are excluded from the hash too. If any of those fields holds an unhashable value such as a list, the class is nominally hashable but `hash(obj)` raises `TypeError` at call time.
  • Are two structurally identical dataclasses with equal field values equal to each other?
    No. The generated `__eq__` first checks that the other operand's class is the same class; for anything else it returns `NotImplemented`, Python falls back, and the comparison is `False`. So two separately declared record types with identical fields never compare equal and never deduplicate against each other in a set, even though their hashes may coincide.

saying these in an interview costs you the question

  • Says any dataclass instance can be a dict key
  • Thinks eq=False makes the class unhashable
  • Treats frozen=True as unrelated to hashing
  • Adds unsafe_hash=True to silence a TypeError
  • Expects two different dataclass types with equal fields to compare equal
  • Forgets a list field makes hash() fail at call time

context