"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
Current CPython dict uses a compact open-addressing design with a defined probe strategy, but that is an implementation detail rather than the only valid Python implementation. Insertion-order preservation has been a language guarantee since Python 3.7; keep that public contract separate from the current internal layout and resize thresholds.