backend / python core / tricky questions / 42_duplicate_keys_in_comprehension.md

Duplicate keys in dict comprehensions

3 min read source

Duplicate keys in dict comprehensions

The gotcha

{f(x): x for x in iter} silently keeps only the last value when f(x) collides. No warning, no error. Different from list/set comprehensions, where everything is preserved.

Minimal repro

d = {x % 3: x for x in range(10)}
print(d)        # {0: 9, 1: 7, 2: 8}
x x%3 written
0 0 d[0] = 0
1 1 d[1] = 1
2 2 d[2] = 2
3 0 d[0] = 3 (overwrites)
4 1 d[1] = 4 (overwrites)
5 2 d[2] = 5 (overwrites)
6 0 d[0] = 6 (overwrites)
7 1 d[1] = 7 (overwrites)
8 2 d[2] = 8 (overwrites)
9 0 d[0] = 9 (overwrites)

The earlier values for x=0..8 are silently dropped.

Why it happens

{k: v for ...} is sugar for:

result = {}
for ... :
    result[k] = v
return result

Each result[k] = v overwrites without check. Standard dict assignment semantics.

By contrast, list comprehensions keep duplicates:

[x % 3 for x in range(10)]   # [0, 1, 2, 0, 1, 2, 0, 1, 2, 0]

And set comprehensions dedupe but the values are the keys:

{x % 3 for x in range(10)}   # {0, 1, 2}

When this matters

Building a lookup from records:

users = [{"id": 1, "name": "Alice"}, {"id": 1, "name": "Bob"}]
by_id = {u["id"]: u for u in users}
# {1: {"id": 1, "name": "Bob"}}   ← Alice silently lost

If duplicates indicate a data bug, surface it:

def index_by(items, key):
    result = {}
    for item in items:
        k = key(item)
        if k in result:
            raise ValueError(f"duplicate key: {k}")
        result[k] = item
    return result

Or aggregate intentionally:

from collections import defaultdict
by_id = defaultdict(list)
for u in users:
    by_id[u["id"]].append(u)
# {1: [{"id": 1, "name": "Alice"}, {"id": 1, "name": "Bob"}]}

What about set comprehensions with duplicate keys?

set and dict.keys() dedupe by hash + eq. So if you want unique keys in a single pass:

ids = {u["id"] for u in users}      # {1}

But you don’t get to pick which item wins for each id — for that, you need a dict comprehension or explicit loop.

“Last wins” can be the feature

When the input is sorted such that the last value is the canonical one (latest update, highest priority), this behavior is what you want:

events = sorted(events, key=lambda e: e.timestamp)
latest_per_user = {e.user_id: e for e in events}    # last (newest) wins

For “first wins”:

events = sorted(events, key=lambda e: e.timestamp, reverse=True)
first_per_user = {e.user_id: e for e in events}     # first (newest, after reverse) wins

Or:

first = {}
for e in events:
    if e.user_id not in first:
        first[e.user_id] = e

Interview angle

  • Q: “What does {x % 3: x for x in range(10)} produce?” — {0: 9, 1: 7, 2: 8}. Last value per key wins.
  • Q: “How would you keep all values per key instead?” — defaultdict(list) and append.
  • Follow-up: “How would you detect duplicate keys at build time?” — explicit loop with if k in result: raise.
  • Follow-up: “Difference from list comprehension?” — list keeps duplicates, dict overwrites.

See stdlib/01_collections.md, 43_inverting_a_dict.md.