Which @dataclass flags decide whether its instances are hashable?
answer
- Two flags decide it, not one
- Defining equality costs you the inherited hash
- Immutability is what buys the hash back
- eq=False keeps the identity hash
- unsafe_hash trades away hash stability
basics
~10 sWith 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 sThree 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 linesfrom 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 tuplego deeper
Recall the symptom: putting a plain dataclass instance in a set raises TypeError: unhashable type. Know that adding frozen=True is the usual fix.
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.
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.
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