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.