Back to Blog
Python

Python Hashable Types: Dict Keys and Set Members

python hashable types: Understand what makes a Python type hashable, why dict keys and set members must be hashable, and how to implement __hash__ and __eq__ correctly.

hashabledict keysset membership__hash____eq__Python data model
Illustration of Python hashable types showing a dictionary and set with a hash function symbol

python hashable types requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

In Python, hashable types are those that can be used as keys in dictionaries and as members of sets. The language requires that an object's hash value remain stable for its lifetime, and that equal objects produce equal hashes. This rule is enforced by the __hash__ and __eq__ methods, and violating it leads to subtle bugs that are hard to trace.

What Makes a Type Hashable in Python

A type is hashable if its instances have a __hash__ method that returns an integer, and an __eq__ method that defines equality. The two methods must be consistent: if a == b is True, then hash(a) == hash(b) must also be True. This contract is what allows dictionaries and sets to locate objects quickly by their hash value.

Most immutable built-in types are hashable. Integers, floats, strings, tuples, and frozenset instances all have stable hashes because their contents cannot change after creation. Mutable types like lists, dictionaries, and sets are not hashable because their hash would change if the contents changed, breaking the contract.

Built-in Hashable and Unhashable Types

TypeHashableReason
intYesImmutable
floatYesImmutable
strYesImmutable
bytesYesImmutable
tupleYesImmutable, but only if all elements are hashable
frozensetYesImmutable
listNoMutable
dictNoMutable
setNoMutable
bytearrayNoMutable
Custom class (default)YesInherits object.__hash__ and object.__eq__ (identity-based)

A tuple is hashable only if every element it contains is hashable. For example, (1, [2, 3]) is not hashable because the list inside is mutable. This is a common source of confusion when using tuples as dictionary keys.

The Hash Contract: hash and eq Consistency

The hash contract is not optional. If you define __eq__ in a custom class without also defining __hash__, Python sets __hash__ to None, making the instances unhashable. This is a deliberate safety measure: if equality is based on object attributes, the default identity-based hash would be inconsistent.

class Point: def __init__(self, x, y): self.x = x self.y = y def __eq__(self, other): if not isinstance(other, Point): return NotImplemented return self.x == other.x and self.y == other.y # Point instances are unhashable because __eq__ is defined without __hash__ p = Point(1, 2) # hash(p) # TypeError: unhashable type: 'Point'

To make this class hashable, you must define __hash__ explicitly. The simplest correct approach is to return the hash of a tuple containing the attributes that define equality:

class Point: def __init__(self, x, y): self.x = x self.y = y def __eq__(self, other): if not isinstance(other, Point): return NotImplemented return self.x == other.x and self.y == other.y def __hash__(self): return hash((self.x, self.y))

Now two Point objects with the same coordinates are equal and have the same hash, so they can be used as dictionary keys or set members.

Making Custom Classes Hashable

When you implement __hash__, you must ensure that the hash value is based on the same attributes used in __eq__. If you change one without the other, you break the contract. For example, if equality checks x and y, but the hash only uses x, then two points with the same x but different y will collide and also be unequal, which is allowed but degrades performance. Worse, if equality uses mutable attributes, the hash becomes unstable.

For a class with multiple attributes, the common pattern is to combine them into a tuple and hash that tuple. This works because tuples are hashable when their elements are. If an attribute is mutable, you should not include it in __hash__ or __eq__ unless you can guarantee the object's hash never changes after insertion into a set or dictionary.

class User: def __init__(self, username, email): self.username = username self.email = email def __eq__(self, other): if not isinstance(other, User): return NotImplemented return (self.username, self.email) == (other.username, other.email) def __hash__(self): return hash((self.username, self.email))

If you need a class to be mutable but still usable as a dictionary key, you must ensure that the attributes used for hashing and equality are immutable for the object's lifetime. In practice, this means freezing those attributes after construction, or using a separate immutable identifier.

Hashability and Mutability: The Hidden Danger

Even if a class is technically hashable, using a mutable instance as a dictionary key can corrupt the dictionary. Consider a list stored as an attribute that participates in equality and hashing:

class BadKey: def __init__(self, items): self.items = items def __hash__(self): return hash(tuple(self.items)) def __eq__(self, other): return self.items == other.items key = BadKey([1, 2]) d = {key: "value"} key.items.append(3) # Mutate the key after insertion # Now d[key] raises KeyError because the hash changed

This is why Python's built-in mutable types are unhashable. If you need a mutable object as a key, the safest approach is to use an immutable proxy, such as a tuple of the relevant fields, or to freeze the object after creation.

Performance and Collision Behavior

The performance of dictionaries and sets depends on the quality of the hash function. Python's built-in hash for strings and numbers is designed to distribute values evenly, but custom __hash__ implementations can be poor. If many objects produce the same hash, they end up in the same hash bucket, turning O(1) lookups into O(n) linear scans.

A common mistake is to return a constant hash, such as return 0. This is technically correct because all equal objects have the same hash, but it destroys performance. For a class with a small number of possible attribute combinations, a simple tuple hash is usually sufficient. For larger or more complex objects, consider combining hashes using a bitwise XOR or a weighted sum, but avoid premature optimization unless profiling shows a problem.

Python's hash function for tuples and frozensets already handles combination well, so delegating to hash(tuple(self.attributes)) is both safe and efficient for most cases.

When to Use Hashable Types (or Not)

Hashable types are essential for dictionary keys and set members, but they are not always the right choice. If you need to store objects in a list and compare them by identity, a custom class without __eq__ and __hash__ is fine. If you need to deduplicate objects by value, a set requires hashable elements.

For data that changes frequently, using an immutable tuple or a frozenset as a key is often simpler than trying to make a mutable class hashable. For example, if you are building a cache keyed by a set of parameters, a tuple of the parameters is a natural hashable key.

When you do implement __hash__ and __eq__, keep them consistent and document the invariant that the hashable attributes are immutable. This prevents the class from being misused in ways that corrupt dictionaries or sets.

A practical pattern for a mutable class that needs to be used as a key is to store an immutable identifier that does not change:

class Record: def __init__(self, id, data): self.id = id # immutable after construction self.data = data # mutable, but not used in hash/eq def __hash__(self): return hash(self.id) def __eq__(self, other): return self.id == other.id

This keeps the hash stable while allowing the object to carry mutable payload. The tradeoff is that equality is based solely on the identifier, so two records with the same id but different data are considered equal. That is usually acceptable when the id is unique.

In summary, understanding python hashable types means knowing not only which built-ins are hashable, but also how to correctly implement the hash contract for your own classes. The rules are simple: equal objects must have equal hashes, and the hash must not change while the object is in a set or dictionary. Respect those rules, and your code will behave predictably.

python hashable types: Practical Usage and Code Examples | RYUSLOG DEV