"Collisions aren't a bug to prevent — they're a certainty to manage. There are more possible keys than slots, so two keys WILL land together. The only question is what you do when they do."
Collisions Are Guaranteed
A table has a fixed number of slots, but the universe of possible keys is effectively infinite. By the pigeonhole principle, two different keys must eventually hash to the same slot. So a hash map's design isn't about avoiding collisions (impossible) — it's about resolving them gracefully. There are two classic strategies, and they make opposite trade-offs.
Chaining: a Little List in Each Slot
Separate chaining makes each slot hold a small list (a "chain") of all the keys that hashed there. To insert, append to the slot's list; to look up, hash to the slot and scan its short list. As long as the lists stay short (good hash + low load), lookups are O(1). Chaining is simple, degrades gracefully under high load (the lists just get a bit longer), and is forgiving of a mediocre hash function. The cost: extra memory for the list/pointers, and the cache-unfriendliness of those scattered list nodes.
Open Addressing: Everybody Lives in the Array
Open addressing stores every entry directly in the table array — no side lists. On a collision, it probes for the next free slot by a fixed rule: linear probing checks slot+1, slot+2, …; quadratic probing jumps by growing gaps; double hashing uses a second hash for the step. Lookups follow the same probe sequence until they find the key or an empty slot. Open addressing is cache-friendly (everything's contiguous, no pointer-chasing) and uses less memory — but it degrades sharply as the table fills (probe sequences get long, a problem called clustering), and deletion is fiddly: you can't just empty a slot or you'd break probe chains, so you mark it with a "tombstone."
What Python Actually Does
CPython's dict and set use open addressing with a clever perturbation-based probe sequence, kept fast by keeping the table at most about two-thirds full (we'll see why in the load-factor lesson). So when you use a Python dict — which is constantly — you're using open addressing with tombstone deletion under the hood. You don't have to implement it, but knowing it explains the dict's behavior: why it resizes, why it's cache-fast, and why insertion order is preserved (a separate compact-array trick layered on top).