dataclasses: Default Instances Are Unhashable by Default (tested)

A plain @dataclass gets __eq__ for free, so two instances with the same field values compare equal. We measured whether it also gets a usable __hash__ -- the kind you'd need to put instances in a set or use as a dict key -- and what happens if you force one with unsafe_hash=True.

1. A plain @dataclass instance is unhashable, even though == works

from dataclasses import dataclass

@dataclass
class Point:
    x: int
    y: int

p = Point(1, 2)
print(p == Point(1, 2))
hash(p)
True
TypeError: unhashable type: 'Point'

Measured: with the default decorator options (eq=True, frozen=False), dataclass sets __hash__ to None -- equality works, but hash(), set(), and using the instance as a dict key all raise TypeError immediately. This is intentional per the docs (a mutable object that compares by value shouldn't silently hash by identity), but it surprises anyone used to plain classes or namedtuples, which hash by identity or value by default without complaint.

2. frozen=True gives a safe hash; unsafe_hash=True gives a hash that changes on mutation

from dataclasses import dataclass

@dataclass(frozen=True)
class FPoint:
    x: int
    y: int

fp = FPoint(1, 2)
print(hash(fp))  # works -- can't mutate, so hash is stable

@dataclass(unsafe_hash=True)
class UPoint:
    x: int
    y: int

up = UPoint(1, 2)
s = {up}
print(hash(up))
up.x = 99
print(hash(up))
print(up in s)
-3550055125485641917
-3550055125485641917
-5595863157221763179
False

Measured: frozen=True blocks mutation entirely, so its hash is always safe. unsafe_hash=True forces a hash onto a still-mutable class -- the name is a real warning, not decoration. After inserting up into a set and then mutating one field, hash(up) changes and up in s returns False even though up is the exact same object that was just inserted. The set now silently contains an item that can no longer be found by its own identity.

3. The fix: use frozen=True for anything you'll hash, or implement __hash__ yourself

from dataclasses import dataclass

@dataclass(frozen=True)
class SafePoint:
    x: int
    y: int

sp = SafePoint(1, 2)
s = {sp}
print(sp in s)  # always True -- SafePoint can never mutate after creation
True

Measured: if you need a dataclass that works correctly in a set or as a dict key, frozen=True is the option that's actually safe -- it prevents the mutation that would otherwise strand the object in whatever hash-based container it was placed in. Reach for unsafe_hash=True only when you're certain the fields used in the hash will never change after the object is first hashed.

Frequently asked questions

Why does hash() raise TypeError on a plain Python dataclass?

Because @dataclass with its default options (eq=True, frozen=False) explicitly sets __hash__ = None -- we measured this raising TypeError('unhashable type') even though == between two instances with equal fields correctly returns True.

How do I make a dataclass usable as a dict key or set element?

Use @dataclass(frozen=True), which both blocks mutation and gives a stable __hash__ based on field values -- we confirmed instances of a frozen dataclass hash correctly and can be looked up from a set reliably.

What does unsafe_hash=True actually do differently from frozen=True?

It forces a __hash__ method onto a dataclass that is still mutable -- we measured that mutating a field after hashing an unsafe_hash instance changes its hash value, so a set already containing that exact object can no longer find it with 'in'.

Is it safe to put a mutable @dataclass instance in a Python set?

Only if you never mutate it after insertion -- we measured that mutating a field of an unsafe_hash dataclass instance already in a set breaks lookup for that object, silently leaving it unreachable even though it's still present.

Does a dataclass with eq=False support hashing by default?

Yes -- setting eq=False on @dataclass leaves __hash__ at the default identity-based hash from object, since the None override only happens when eq=True and frozen=False together, per the dataclasses module's documented rule.

Written with AI assistance. Measured on 2026-10-04 with Python 3.13.5's built-in dataclasses module: created a plain @dataclass, tried to hash it and put it in a set, then compared against @dataclass(frozen=True) and @dataclass(unsafe_hash=True), including mutating an unsafe_hash instance after inserting it into a set. PlainHeaders is our extension; Python's dataclasses module is built in.