backend / python core / tricky questions / 43_inverting_a_dict.md

Inverting a dict — what could go wrong

3 min read source

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.