backend / python core / 18_dict_hashable_objects.md

Hashable Objects in Python

3 interview angles 3 min read source

Hashable Objects in Python

What is a Hashable Object?

In Python, an object is hashable if:

  • It has a hash value that remains constant during its lifetime (__hash__() method).
  • It can be compared to other objects (__eq__() method).
  • It is immutable (its state cannot change).

Hashable objects can be used as keys in dictionaries and elements in sets.


What Can Be Used as a Dictionary Key?

Only hashable objects can be used as keys in dictionaries.

Examples of hashable (valid key) types:

  • int
  • float
  • str
  • tuple (if all its elements are hashable)
  • frozenset
  • bool
  • NoneType
my_dict = {
    42: "an int",
    "name": "a string",
    (1, 2): "a tuple",
    frozenset([1, 2, 3]): "a frozenset"
}

Examples of unhashable (invalid key) types:

  • list
  • dict
  • set
# This will raise a TypeError
my_dict = {
    [1, 2, 3]: "a list",  # unhashable
}

Tuple containing a list — also unhashable

A tuple is hashable only if all its elements are hashable. tuple.__hash__ recursively hashes each element; if any element raises TypeError on hash(), the whole tuple does too.

d = {(1, [2, 3]): "x"}
# TypeError: unhashable type: 'list'
hash((1, 2, 3))           # — all elements hashable
hash((1, (2, 3)))         # — nested tuple, all hashable
hash((1, [2, 3]))         # TypeError — list inside breaks it
hash((1, frozenset([2, 3])))  # — frozenset is hashable

The check happens at insertion time, not at definition time — so the error fires when you try to put the tuple into a dict or set.

Why mutability rules out hashability

If a key’s hash could change after insertion, the dict would lose track of it. The bucket is selected by hash at insert; a later hash change means lookup goes to the wrong bucket. The runtime can’t catch this — so it forbids hashable+mutable combinations entirely.

That’s why:

  • list (mutable) is unhashable
  • tuple (immutable shell) is hashable, but only if elements are too — a tuple([2, 3]) would still be mutable through its element
  • frozenset is hashable; set is not
  • Custom classes with @dataclass(frozen=True) are hashable; default @dataclass is not (Python sets __hash__ = None)

See 23_eq_vs_hash.md for the full hash/eq contract and tricky_questions/34_mutating_dict_key_after_insertion.md for what goes wrong if you bypass it.


What Can Be Used as a Dictionary Value?

Any Python object can be used as a value in a dictionary — there are no restrictions.

my_dict = {
    "username": "danil",
    "score": 95,
    "tags": ["python", "dev"],
    "profile": {"age": 25, "country": "UA"},
    "callback": lambda x: x * 2
}

Interview angle

  • “What makes an object usable as a dict key?” - hashable: it implements __hash__ and its hash never changes while it’s in use. Mutable built-ins like list and dict are deliberately unhashable.
  • “What’s the __eq__/__hash__ contract?” - equal objects must have equal hashes. Defining __eq__ without __hash__ sets __hash__ to None and makes the class unhashable, which is Python protecting you from a broken contract.
  • “What happens if a key mutates after insertion?” - its hash changes, so it lands in the wrong bucket and becomes unfindable - even by itself. This is why frozen=True dataclasses are the right choice for keys.