Python dict hash collision slowdown: detection and solution

Slow dict lookups in Python often appear in production code that builds index maps for large pandas DataFrames, where many keys share the same hash bucket. This forces the interpreter to scan long chains, leading to noticeable latency in data‑heavy pipelines.

# Example showing the issue
import time

class BadKey:
    def __init__(self, value):
        self.value = value
    def __hash__(self):
        return 42  # constant hash forces collisions
    def __eq__(self, other):
        return isinstance(other, BadKey) and self.value == other.value

# 100,000 colliding keys
keys = [BadKey(i) for i in range(100_000)]
colliding_dict = {k: i for i, k in enumerate(keys)}
start = time.time()
for k in keys:
    _ = colliding_dict[k]
print(f"Lookup time with collisions: {time.time() - start:.3f}s")

# Good keys (int) with proper hash distribution
good_keys = list(range(100_000))
good_dict = {k: i for i, k in enumerate(good_keys)}
start = time.time()
for k in good_keys:
    _ = good_dict[k]
print(f"Lookup time with good hash: {time.time() - start:.3f}s")
# Expected output shows the colliding version several times slower than the good version

When many keys produce the same hash value, CPython stores them in a single bucket and falls back to a linear search for each lookup, turning O(1) access into O(n). This behavior mirrors the hash‑table design described in the official Python documentation and often surprises developers who assume constant‑time lookups regardless of key distribution. Related factors:

  • User‑defined hash returning constant values
  • Large numeric ranges that trigger hash collisions on 32‑bit builds
  • Disabled hash randomization for strings

To diagnose this in your code:

# Count how many keys share each hash value
hash_counts = {}
for k in colliding_dict.keys():
    h = hash(k)
    hash_counts[h] = hash_counts.get(h, 0) + 1
max_collisions = max(hash_counts.values())
print(f"Maximum collisions in dict: {max_collisions}")
# If max_collisions >> 1 you likely have a performance problem

Fixing the Issue

Quick Fix (1‑Liner)

# Convert problematic keys to a type with a better hash (e.g., string)
fixed_dict = {str(k): v for k, v in colliding_dict.items()}

When to use: Debugging or small scripts where key semantics are simple. Trade‑off: May increase memory usage and changes key type.

Best Practice Solution (Production‑Ready)

import logging
import os

# 1. Detect collisions
hash_counts = {}
for k in colliding_dict:
    h = hash(k)
    hash_counts[h] = hash_counts.get(h, 0) + 1
if max(hash_counts.values()) > 1:
    logging.warning("Hash collision detected: %s max collisions", max(hash_counts.values()))

# 2. Replace keys with a robust hash implementation
class RobustKey:
    def __init__(self, value):
        self.value = value
        # Mix in a random per‑process seed to break patterns
        self._seed = int.from_bytes(os.urandom(4), "little")
    def __hash__(self):
        return hash((self.value, self._seed))
    def __eq__(self, other):
        return isinstance(other, RobustKey) and self.value == other.value

robust_dict = {RobustKey(k.value): v for k, v in colliding_dict.items()}

# 3. Validate that lookups are now fast
import timeit
print("Fast lookup time:", timeit.timeit(lambda: list(robust_dict.values())[0], number=1000))

When to use: Production pipelines, long‑running services, or any code path where dicts may hold millions of entries. Why better: Logs the problem, injects per‑process randomness to avoid crafted collisions, and keeps the original key semantics while restoring O(1) performance.

What Doesn’t Work

❌ Using dict.copy() after insertion: This copies the same colliding keys, so lookup speed stays degraded

❌ Shuffling keys before building the dict: Order changes but the underlying hash buckets remain the same, offering no speed gain

❌ Setting PYTHONHASHSEED=0 to get deterministic hashes: This can actually increase collision risk for certain key patterns

  • Using a custom class with a constant hash without realizing the performance impact
  • Assuming dict lookups are always O(1) even with maliciously crafted keys
  • Disabling hash randomization (PYTHONHASHSEED) to get reproducible hashes, which can amplify collisions

When NOT to optimize

  • Small dictionaries: Fewer than a few thousand entries, the overhead of extra hashing is negligible
  • One‑off analysis scripts: Temporary notebooks where execution time is not critical
  • Known one‑to‑many mappings: When the data model intentionally creates many keys that hash alike and you accept the cost
  • Read‑only caches: Pre‑populated structures that never change after start‑up

Frequently Asked Questions

Q: Can I turn off hash randomization to avoid collisions?

No; disabling it makes crafted collisions easier, worsening the problem.

Q: Do Python sets suffer from the same issue?

Yes, because sets share the same hash‑table implementation as dicts.


Hash collisions are a hidden performance sink in data‑intensive Python applications. By auditing your key objects and injecting per‑process randomness, you restore the expected constant‑time behavior of dicts. Treat collision detection as part of your regular performance‑testing suite to avoid surprises in production.

Why dataclass slower than namedtuple in PythonFix Python dict get vs [] KeyErrorWhy pandas merge on categorical columns slows downWhy pandas concat uses more memory than append