What makes an object hashable, and how does that relate to `__eq__`?
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.