Python Dictionary Key Requirements
python dictionary key requirements: Understand why Python dictionary keys must be hashable, immutable, and unique, and how to correctly use custom objects as keys.
python dictionary key requirements requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
Python dictionaries require keys to be hashable, immutable, and unique. These three requirements determine what can be used as a key and how the dictionary behaves at runtime. Violating any of them leads to a TypeError, unexpected equality behavior, or data corruption. This article explains the technical reasons behind each requirement and shows how to work with them correctly in real code.
What Makes a Key Hashable
A hashable object has a __hash__ method that returns an integer, and an __eq__ method that defines equality. The hash value must remain constant for the object's lifetime. Python uses this hash to place and find entries in the dictionary's internal hash table.
All built-in immutable types are hashable: int, float, str, bytes, tuple (if its elements are hashable), and frozenset. Mutable containers like list, set, and dict are not hashable because their contents can change, which would alter their hash value and break the dictionary's internal structure.
valid_keys = {1: 'int', 3.14: 'float', 'text': 'str', (1, 2): 'tuple'} # Attempting to use a list raises TypeError # invalid = {[1, 2]: 'list'} # TypeError: unhashable type: 'list'
When you insert a key, Python calls hash(key) and stores the result. On lookup, it computes the hash again and compares it with stored hashes to find the correct bucket. If the hash changes after insertion, the key becomes unreachable because its stored hash no longer matches its current hash.
Immutable Types as Keys
Immutability is not strictly required by the dictionary implementation, but it is a practical necessity. If a key's value changes, its hash might change, and the dictionary would no longer be able to locate the entry. For this reason, Python only allows hashable types, and hashable types are typically immutable.
Strings and integers are the most common keys because they are immutable and have well-defined hash functions. Tuples are also useful when you need a composite key, but only if all elements inside the tuple are hashable.
point = (3, 4) coordinates = {point: 'origin offset'} print(coordinates[(3, 4)]) # Works because tuple hashing is deterministic
A tuple containing a list is not hashable because the list inside it is mutable. The same rule applies to nested structures: every level must be hashable for the outer object to be usable as a key.
Uniqueness and Equality Semantics
Dictionary keys must be unique. When you insert a key that already exists, the dictionary replaces the value instead of adding a new entry. Uniqueness is determined by equality, not by identity. Two distinct objects that compare equal are treated as the same key.
a = (1, 2) b = (1, 2) d = {a: 'first'} d[b] = 'second' print(d) # {(1, 2): 'second'}
Here a and b are different objects but compare equal, so the second assignment updates the existing entry. This behavior is essential for using tuples as composite keys: you can construct a new tuple with the same values and it will match the existing key.
Equality also affects hash collisions. If two keys have the same hash but are not equal, Python stores both in the same bucket and uses __eq__ to distinguish them. This is normal and does not cause errors, but it can degrade lookup performance if many collisions occur.
Common Mistakes with Mutable Keys
Attempting to use a mutable object directly as a key raises a TypeError at the point of insertion. This is the most common mistake developers encounter.
# This fails immediately # d = {[1, 2]: 'value'} # TypeError: unhashable type: 'list'
A subtler problem occurs when you use a custom class that is hashable but whose attributes can change. Even if the class defines __hash__, modifying an attribute that participates in equality or hashing will break the dictionary.
class Point: def __init__(self, x, y): self.x = x self.y = y def __hash__(self): return hash((self.x, self.y)) def __eq__(self, other): return (self.x, self.y) == (other.x, other.y) p = Point(1, 2) d = {p: 'origin'} p.x = 10 # Now the hash changes, but the dictionary still has the old hash # d[Point(10, 2)] will not find the entry
To avoid this, make your custom key objects immutable by not exposing setters or by using namedtuple or dataclass(frozen=True).
Performance Considerations of Key Lookup
Dictionary lookups are O(1) on average because they rely on hash values. The quality of the hash function directly affects performance. Python's built-in hash functions for strings and numbers are well-distributed, but custom __hash__ implementations can be poor if they return constant values or use only a subset of the object's data.
A bad hash function causes many collisions, turning lookups into O(n) in the worst case. For example, returning 0 from __hash__ makes every key land in the same bucket, destroying the performance advantage.
class BadKey: def __hash__(self): return 0 # All keys collide
When designing custom keys, combine the hashes of the fields that define equality. Use hash((self.attr1, self.attr2)) rather than a simple sum, because tuples have a well-tested hash algorithm. Avoid using mutable attributes in the hash computation.
Custom Objects as Keys
To use a custom object as a dictionary key, you must implement both __hash__ and __eq__. If you override __eq__ without overriding __hash__, Python sets __hash__ to None, making the object unhashable.
class User: def __init__(self, user_id): self.user_id = user_id def __hash__(self): return hash(self.user_id) def __eq__(self, other): return isinstance(other, User) and self.user_id == other.user_id users = {User(1): 'Alice', User(2): 'Bob'}
When two objects compare equal, they must have the same hash. Violating this rule breaks the dictionary contract and can cause entries to be unreachable. The inverse is not required: different objects can have the same hash, which is just a collision.
For simple immutable data holders, prefer dataclass(frozen=True) or namedtuple. They generate correct __hash__ and __eq__ methods automatically, reducing the risk of subtle bugs.
Edge Cases and Compatibility
Floating-point numbers as keys can behave unexpectedly because of precision issues. Two values that are mathematically equal might have different binary representations, or float('nan') is not equal to itself, so it cannot reliably be used as a key.
nan = float('nan') d = {nan: 'value'} print(nan in d) # False, because nan != nan
Tuples containing mutable objects are not hashable, but tuples containing only immutable objects are fine. frozenset can be used as a key when you need an unordered collection of hashable elements.
When working with Python versions, note that hash randomization is enabled by default for strings and bytes. This means string hashes differ between processes, but it does not affect dictionary correctness—only the internal bucket order. Your code should never rely on the exact integer value returned by hash().
Finally, if you need to use a dictionary as a key, you can convert it to a frozenset of its items or to a tuple of sorted key-value pairs, but only if the values are hashable. This approach is useful for memoization or caching when the input is a dictionary.
def dict_key(d): return frozenset(d.items()) cache = {} params = {'a': 1, 'b': 2} key = dict_key(params) cache[key] = 'result'
This works because frozenset is hashable and the items are tuples, which are hashable if the values are. The order of the dictionary does not matter because frozenset is unordered.