Inverting a dict — what could go wrong
The gotcha
{v: k for k, v in d.items()} looks innocent. Two failure modes: non-unique values silently drop entries, and non-hashable values raise TypeError.
Minimal repro
Failure mode 1: collisions
d = {"a": 1, "b": 2, "c": 1}
inverted = {v: k for k, v in d.items()}
print(inverted) # {1: 'c', 2: 'b'} ← 'a' lost
'a' and 'c' both map to 1. The comprehension processes in order, last wins. The mapping 1 → 'a' is silently overwritten.
Failure mode 2: unhashable values
d = {"a": [1, 2], "b": [3, 4]}
inverted = {v: k for k, v in d.items()}
# TypeError: unhashable type: 'list'
Values become keys; keys must be hashable. Lists, sets, dicts can’t be keys.
How to handle both
Multi-valued inverse (collisions OK)
from collections import defaultdict
d = {"a": 1, "b": 2, "c": 1}
inverted = defaultdict(list)
for k, v in d.items():
inverted[v].append(k)
print(dict(inverted)) # {1: ['a', 'c'], 2: ['b']}
Now no entries lost. Each key in the inverted dict maps to all originals.
Detect collisions and raise
def invert(d):
out = {}
for k, v in d.items():
if v in out:
raise ValueError(f"non-unique value: {v} appears multiple times")
out[v] = k
return out
Use when “every value should be unique” is a real invariant.
Hash unhashable values via tuple/frozenset coercion
d = {"a": [1, 2], "b": [3, 4]}
inverted = {tuple(v): k for k, v in d.items()}
# {(1, 2): 'a', (3, 4): 'b'}
Or frozenset if order doesn’t matter.
This loses information (you can’t recover the original list type without remembering it), but for use cases where you only need key_for_value(v) lookup, it works.
Bidict — the dedicated library
from bidict import bidict
d = bidict({"a": 1, "b": 2})
d["a"] # 1
d.inverse[1] # "a"
d["c"] = 1 # ValueDuplicationError — raises by default
bidict enforces uniqueness in both directions and gives you inverse for O(1) reverse lookup. Worth pulling in for any non-trivial bidirectional mapping.
Real-world: enum-like lookups
HTTP_STATUS = {
200: "OK",
301: "Moved Permanently",
404: "Not Found",
}
# Forward: code → name
HTTP_STATUS[200] # "OK"
# Reverse: name → code
NAME_TO_CODE = {v: k for k, v in HTTP_STATUS.items()}
NAME_TO_CODE["Not Found"] # 404
Safe because HTTP status names are unique by spec. For user-supplied data, use one of the patterns above.
Inversion and order
Python 3.7+ preserves insertion order. The inverted dict’s order reflects the order values were first seen during iteration of the original — which equals original insertion order in single-pass dict comprehension. So:
d = {"first": 1, "second": 2, "third": 3}
{v: k for k, v in d.items()}
# {1: 'first', 2: 'second', 3: 'third'} ← order preserved
When values collide, the later writes don’t change order — they update value at the existing key position.
Interview angle
- Q: “How do you invert a dict?” —
{v: k for k, v in d.items()}. - Q: “What can go wrong?” — non-unique values silently drop entries; non-hashable values raise.
- Follow-up: “How would you handle non-unique values?” —
defaultdict(list)to accumulate, or raise. - Follow-up: “What library is built for bidirectional dicts?” —
bidict.
See 42_duplicate_keys_in_comprehension.md, stdlib/01_collections.md, 23_eq_vs_hash.md.