A hash table stores key-value pairs so that a value can be found from its key in about the same time no matter how many pairs there are. Python's dict and set are hash tables, which is why reservations["J4"] and "J4" in codes stay fast with millions of entries:
reservations = {"F7": {"seats": 2}, "J4": {"seats": 5}}
print(reservations["J4"]["seats"]) # 5: found by hashing "J4"This lesson explains what happens underneath: how a hash turns a key into a position, what happens when two keys collide, why only hashable values can be keys, and what the famous O(1) average lookup assumes. The examples index table reservations at a restaurant. You already know how to use dictionaries; here you'll learn why they behave as they do.
Hashing
A hash function turns a key into an integer. Python's built-in hash() exposes it:
print(hash(42)) # 42: small ints hash to themselves
print(hash("J4") == hash("J4")) # True within one runThe hash table is an array of slots. To store a key, Python computes its hash and reduces it to a slot index, roughly hash(key) % number_of_slots. To look the key up later, it computes the same hash and goes straight to that slot instead of scanning every entry. That's where the speed comes from.
A hash is not a unique identifier. There are infinitely many possible strings but only a fixed range of integers, so different keys must sometimes share a hash. String hashes are also randomised per process for security (to stop attackers deliberately creating collisions), so hash("J4") changes between program runs. Never store hash values in files or databases expecting them to match later.
Collisions
A collision happens when two different keys map to the same slot, either because their hashes are equal or because different hashes reduce to the same index. Collisions are normal, and every hash table has a strategy for them.
Python uses open addressing: if the slot for a key is taken by a different key, it probes other slots in a pseudo-random sequence until it finds the key or an empty slot. To confirm it has found the right entry, it compares the stored hash and then uses == on the keys:
lookup "J4": hash -> slot 5 -> holds "Q2" (different key) -> probe slot 2 -> holds "J4" -> foundBecause equality decides the final match, keys that are equal must have equal hashes; otherwise Python would look in the wrong slot. That's the contract Python enforces on hashable objects. Tables are kept partly empty so probe sequences stay short: when a dict gets about two-thirds full, Python allocates a larger table and reinserts everything, which is called resizing.
Hashable keys
A key must be hashable: it needs a hash that never changes while it's stored, and an __eq__ consistent with that hash. In practice:
| Hashable (valid keys) | Unhashable |
|---|---|
int, float, str, bool, None | list |
tuple of hashable items | dict |
frozenset | set |
| your objects, by default (hash by identity) | objects defining __eq__ without __hash__ |
by_table = {("Fri", 7): "Okafor"} # tuple key: day and table number
by_table[["Fri", 7]] = "Ruiz" # TypeError: unhashable type: 'list'A list can't be a key because it can change: if it changed after being stored, its hash would change and the entry would be lost in the wrong slot. A tuple of immutable values is the standard compound key. If you define __eq__ on a class, also define __hash__ from the same immutable fields, or Python sets __hash__ to None, as explained in magic methods.
Cost assumptions
Dictionary and set operations, such as lookup, insert, delete and in, are O(1) on average. That rests on assumptions:
- the hash function spreads keys evenly across slots;
- the table is kept sparse enough by resizing;
- comparing two keys is cheap.
Resizing costs O(n) when it happens, but it happens rarely and the size grows geometrically, so averaged over many insertions each one is still O(1) (amortised). The worst case for a single lookup is O(n), if very many keys collide, which is why Python randomises string hashes.
codes = [f"R{i}" for i in range(100_000)]
code_set = set(codes)
print("R99999" in codes) # True, after scanning ~100,000 items
print("R99999" in code_set) # True, after hashing onceIn everyday code the average case holds, which is why the advice "use a set for membership and a dict for lookups by key" is so reliable. Hash tables do use extra memory, and they don't keep keys sorted; for ordered queries, such as ranges, combine them with sorting or bisect.
Independent practice and a worked check
Task: index reservations by code and explain hashability, collision handling and average versus worst-case cost.
reservations = [("F7", "Okafor", 2), ("J4", "Ruiz", 5), ("Q2", "Chen", 4)]
by_code = {code: (name, seats) for code, name, seats in reservations}
by_slot = {("Fri", 7): "F7", ("Sat", 2): "J4"} # compound tuple keys
print(by_code["J4"]) # ('Ruiz', 5)
print(by_code.get("Z9", "no booking")) # no booking
print(by_code[by_slot[("Fri", 7)]][0]) # Okafor
print(all(hash(k) == hash(k) for k in by_code)) # equal keys, equal hashesExplain in comments: lookups are O(1) on average because each key's hash leads straight to its slot; collisions are resolved by probing and equality checks; the worst case is O(n) if many keys collide. For the worked check: can different dictionary keys have the same hash? Yes. Collisions are expected and handled by the table.
Reference and next practice
The documentation defines hashable and the __hash__ method; the Python wiki lists dict and set operation costs. Continue with trees and binary search trees. Practise with the Python exercises and Python quiz.
Hash Tables Summary
- Hashing: A hash maps a key to a numeric value used in locating a storage region.
- Collisions: Different keys can share a hash and a table must resolve those collisions.
- Hashable keys: A key needs a stable hash and a compatible equality contract while stored.
- Cost assumptions: Dictionary operations are usually average constant time under suitable assumptions.
Hash Tables Examples
Each example shows the code, the output it produces and what each part does.
Hash Tables in a small program
reservations = {"F7": {"seats": 2}, "J4": {"seats": 5}}
print(reservations["J4"]["seats"])
print(reservations.get("Z9", {"seats": 0})["seats"])5
0Line-by-line explanation
- Line 1Each booking code is a key; Python hashes it to find its slot.
- Line 2A direct lookup by key returns the nested dictionary for J4.
- Line 3
get()with a default handles a missing code without raisingKeyError.
A tiny hash table built by hand
class SimpleHashTable:
def __init__(self, size=8):
self.buckets = [[] for _ in range(size)]
def _bucket(self, key):
return self.buckets[hash(key) % len(self.buckets)]
def put(self, key, value):
bucket = self._bucket(key)
for pair in bucket:
if pair[0] == key:
pair[1] = value
return
bucket.append([key, value])
def get(self, key, default=None):
for k, v in self._bucket(key):
if k == key:
return v
return default
table = SimpleHashTable(size=4)
for code, seats in [(11, 2), (15, 4), (19, 6), (12, 3)]:
table.put(code, seats)
table.put(15, 5)
print([len(b) for b in table.buckets])
print(table.get(19), table.get(15), table.get(99, "none"))[1, 0, 0, 3]
6 5 noneLine-by-line explanation
- Lines 5 to 6The hash, reduced modulo the number of buckets, chooses where a key lives.
- Lines 10 to 12Equality, not the hash alone, confirms the right key; an existing key's value is replaced.
- Lines 22 to 24Integer keys 11, 15 and 19 all land in bucket 3 (
hash(n) % 4 == 3): collisions are stored side by side and still found correctly. This sketch uses separate chaining; Python's dict uses open addressing, but the hash-then-compare idea is the same.
Common Hash Tables Mistakes
Using a mutable list as a key
seating = {}
seating[["Fri", 7]] = "Okafor" # TypeError: unhashable type: 'list'seating = {}
seating[("Fri", 7)] = "Okafor" # tuples of immutable values are hashableAssuming hash values are unique or stable
ids = {hash(name): name for name in guests} # two names may share a hash
save_to_file(hash("J4")) # string hashes change between runsids = {name: name for name in guests} # let the dict handle hashing
save_to_file("J4")Hash Tables in Real Projects
Hash tables in real software
Hash tables are among the most heavily used data structures in computing. Python itself stores module globals, class attributes and object attributes in dictionaries. Caches map request keys to stored responses, databases use hash indexes and hash joins, compilers keep symbol tables, and deduplication tools compare file hashes to find copies.
Cryptographic hashes such as SHA-256, from the hashlib module, are a different tool: they're designed so that collisions are practically impossible to find, which makes them suitable for checksums and signatures, but they're far slower than the hash functions used inside dictionaries.
Hash Tables FAQ
Is a Python dictionary a hash table?
Yes. dict is a hash table using open addressing, with a separate compact array that keeps entries in insertion order. set is also a hash table, storing keys without values.
Why must dictionary keys be immutable?
A key's hash decides where it's stored. If the key could change, its hash would change and Python would look in the wrong place, losing the entry. So keys must be hashable, which in practice means immutable values or tuples of them.
What is a hash collision?
Two different keys mapping to the same slot in the table. It's normal and expected; the table resolves it by probing other slots (Python) or chaining entries in a bucket, then comparing keys with == to find the right one.
Why does hash() of a string change each time I run Python?
Python randomises string and bytes hashes per process to protect against attacks that deliberately cause collisions. Set PYTHONHASHSEED to fix it for debugging, but never rely on hash values persisting between runs.
Are dictionary lookups always O(1)?
On average, yes. A single lookup can take longer when keys collide, and an insertion that triggers a resize costs O(n), but averaged over many operations dicts and sets behave as O(1).
Hash Tables Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
Can different dictionary keys have the same hash?
Which review concern is valid for hash tables?
Hash Tables Quiz
2 questions to check what you learned.
Can different dictionary keys have the same hash?
- No
- Yes
- Python ignores the distinction
Which statement correctly describes hashable keys?
- A key needs a stable hash and a compatible equality contract while stored.
- Only the example output matters, not the rule
- Every operation has the same behavior