What makes an object hashable, and how does that relate to `__eq__`?

5 minintermediatecollectionshashableeq-hash

Quick Answer

An object is hashable if it implements `__hash__` (returning a stable integer for its lifetime) and, if it implements `__eq__`, guarantees that **equal objects have equal hashes**. All immutable built-ins (`int`, `str`, `tuple` of hashables, `frozenset`) are hashable; mutable built-ins (`list`, `dict`, `set`) are not, since mutating them would change their hash — silently breaking dict/set invariants if they were used as keys.

Detailed Answer

What "hashable" requires

hash(42)            # works -- int is hashable
hash("abc")          # works -- str is hashable
hash((1, 2, 3))       # works -- tuple of hashables is hashable
hash([1, 2, 3])       # TypeError: unhashable type: 'list'
hash({1, 2})          # TypeError: unhashable type: 'set'
hash(frozenset({1,2})) # works -- frozenset (immutable set) is hashable

Hashability requires: (1) a __hash__ method that returns the same integer every time for a given object's lifetime, and (2) if __eq__ is defined, a == b implies hash(a) == hash(b) — this is required for correct dict/set behavior (two "equal" keys must land findable in the same bucket).

Why mutable containers are unhashable

lst = [1, 2, 3]
d = {lst: "value"}   # TypeError -- if this worked...

lst.append(4)         # ...and this mutated the key after insertion,
d[lst]                 # the dict's internal slot (based on the OLD hash) would be wrong

If list were hashable and its hash were based on contents, mutating a list already used as a dict key would silently corrupt the dict's internal structure (the key's slot no longer matches its current hash). Python sidesteps this entirely by making mutable containers unhashable.

Custom classes: hashable by default (via identity)

class Point:
    def __init__(self, x, y):
        self.x, self.y = x, y

p = Point(1, 2)
hash(p)   # works! -- default __hash__ is based on id(), inherited from object

By default, custom classes inherit object.__hash__, based on identity (id()) — two distinct Point(1, 2) instances hash differently even though they'd naturally seem "equal." This is fine until you also define __eq__ for value-based equality:

class Point:
    def __init__(self, x, y):
        self.x, self.y = x, y
    def __eq__(self, other):
        return isinstance(other, Point) and (self.x, self.y) == (other.x, other.y)

hash(Point(1, 2))   # TypeError: unhashable type: 'Point'

Defining __eq__ makes Python set __hash__ = None automatically, because the identity-based default hash would now violate the "equal implies equal hash" contract. Fix by defining a consistent __hash__:

    def __hash__(self):
        return hash((self.x, self.y))

Interview-ready summary: Hashable means "has a stable __hash__, and if __eq__ is defined, equal objects hash equally." Built-in mutable containers are unhashable by design (mutation would corrupt any dict/set using them as keys); custom classes are hashable by identity by default, but overriding __eq__ disables that default hash until you provide a matching __hash__ explicitly.