Python dictionary comprehension: syntax, examples & pitfalls
Contents
Dictionary comprehension isn't just syntactic sugar over a for-loop: it changes how CPython allocates and resizes the underlying hash table, which matters once you're optimizing hot paths. Most misuse comes from forcing a comprehension where a for-loop or dict.fromkeys would be clearer and just as fast.
This guide breaks down the exact syntax, shows zip and items patterns for real transformations, and includes timeit numbers so you can decide when comprehension actually wins.
Dictionary comprehension in One line
A dictionary comprehension turns a three-line for-loop into one line: {key: value for item in iterable}. Each key-value pair is built and inserted in a single pass, with no temporary dict call or manual .update step.
Python formalized this syntax in PEP 274, the proposal that introduced dict comprehensions alongside the existing list and set forms. Teams building data pipelines hit this pattern constantly: converting API responses into lookup dictionaries, or applying a discount rate across a list of prices.
Benchmarks in this guide come from timeit runs on CPython 3.11. We cover the syntax, when zip or items beats a comprehension, and where a plain for-loop still reads better.
Dictionary comprehension syntax explained
Every dictionary comprehension breaks down into four labeled parts: {key_expr: value_expr for item in iterable}. The curly braces signal a dictionary, not a list, the first thing to check when reading unfamiliar Python.
- key_expr, the expression that becomes each key (must be hashable)
- value_expr, the expression that becomes the paired value
- item in iterable, the loop that maps input to output, one key-value pair per pass
- an optional trailing if condition, a conditional expression that filters items before they reach the dictionary
prices = {"apple": 1.2, "banana": 0.5, "pear": 2.0}
discounted = {k: round(v * 0.9, 2) for k, v in prices.items() if v > 1}
print(discounted)
# {'apple': 1.08, 'pear': 1.8}
The if v > 1 clause is the conditional expression. It drops the banana entry before a value is ever computed, unlike a for-loop filtered inside the block. PEP 8 recommends wrapping any comprehension exceeding the 79-character line limit across multiple lines for readability, not cramming logic into one dense expression.
Creating a dictionary from two lists with zip
`zip()` is a built-in function that pairs up elements from two or more iterables position by position, stopping as soon as the shortest input runs out. Pairing zip() with a dictionary comprehension is the standard way to build a dictionary from two parallel lists, but dict(zip(keys, values)) is usually the better call when no per-item transformation is needed.
keys = ['sku', 'name', 'price']
values = ['A100', 'Widget', 9.99]
# Dict comprehension, use when you need to transform keys or values
catalog = {k: v for k, v in zip(keys, values)}
# Dict(zip), faster, use for a straight pairing
catalog = dict(zip(keys, values))
print(catalog)
# Output: {'sku': 'A100', 'name': 'Widget', 'price': 9.99}
Both read cleanly and both stop at the shorter list, matching zip's own truncation behavior. Reach for the comprehension only when you're applying a conditional expression or casting values mid-loop, otherwise dict(zip) skips the comprehension overhead entirely.
Swapping keys and values
`dict.items()` is a built-in dictionary method that returns a view of (key, value) tuples, one per entry, which is exactly what a comprehension needs to unpack on each pass. Swap keys and values in a Python dictionary with {v: k for k, v in original.items()}, iterating the source dictionary's key-value pairs and flipping them in a single pass.
prices = {'sku100': 9.99, 'sku200': 14.99, 'sku300': 9.99}
by_price = {v: k for k, v in prices.items()}
print(by_price)
# {9.99: 'sku300', 14.99: 'sku200'}
Notice the silent overwrite: two SKUs shared the value 9.99, and the second key-value pair overwrote the first in the output dictionary. Swapping only produces a clean one-to-one mapping when the original values are unique. If duplicates are possible, check len(set(original.values)) == len(original) first, or build {v: [k for k, k2 in original.items() if k2 == v] for v in set(original.values)} to keep every key.
Filtering and conditional logic inside comprehensions
A dictionary comprehension can filter, transform, or both, and the two forms look almost identical but behave differently. Filter-only drops key-value pairs that fail a condition; the if/else variant keeps every key but rewrites the value with a conditional expression.
prices = {'sku100': 9.99, 'sku200': 14.99, 'sku300': 4.99}
# Filter-only: Keep pairs above a threshold
premium = {k: v for k, v in prices.items() if v > 5.00}
print(premium)
# {'sku100': 9.99, 'sku200': 14.99}
# If/else: Keep every key, transform the value
discounted = {k: (v * 0.9 if v > 5.00 else v) for k, v in prices.items()}
print(discounted)
# {'sku100': 8.991, 'sku200': 13.491, 'sku300': 4.99}
The placement of the conditional gives away which form you're reading. A trailing if after items filters; a conditional expression sitting before the for clause always produces a value, just a different one depending on the branch. Confusing the two during a code review is the most common dictionary comprehension bug we see, since both read as "one line, one condition" at a glance.
Nested dictionary comprehension: Example and refactor
A nested dictionary comprehension builds a dictionary whose values are themselves dictionaries, typically by looping over an outer iterable and running an inner comprehension per key. It reads cleanly for two levels; past that, it stops being a comprehension and becomes a puzzle.
teams = {'eng': ['ana', 'bo'], 'sales': ['cid']}
roster = {team: {name: len(name) for name in members} for team, members in teams.items()}
print(roster)
# {'eng': {'ana': 3, 'bo': 2}, 'sales': {'cid': 3}}
Here the outer comprehension iterates teams.items(), and each value is itself a fresh dictionary comprehension over members, no loop, no temporary list, just nested key-value pair construction.
We treat three levels of nesting as a hard PEP 8 line-length and readability limit. According to PEP 8 guidelines, PEP 8 caps lines at 79 characters, and a triple-nested comprehension rarely fits without wrapping into something a reviewer has to trace by hand.
Past that point, drop back to an explicit for-loop or a generator expression feeding dict, and name the intermediate step.
If you catch yourself nesting a comprehension inside a comprehension inside a conditional expression, that is the signal to unwind it into a named helper function instead.
raw = {'emea': {'warsaw': {'mon': [12, 15], 'tue': [9, 11]}}}
# Flagged in review: three levels deep, hard to trace
report = {
region: {city: {day: round(sum(v) / len(v), 2) for day, v in readings.items()}
for city, readings in cities.items()}
for region, cities in raw.items()
}
# Refactored: named helper pulls the innermost logic out
def daily_averages(readings):
return {day: round(sum(v) / len(v), 2) for day, v in readings.items()}
report = {
region: {city: daily_averages(readings) for city, readings in cities.items()}
for region, cities in raw.items()
}
print(report)
# {'emea': {'warsaw': {'mon': 13.5, 'tue': 10.0}}}
The refactor doesn't remove the nesting, it names the innermost step. A reviewer scanning daily_averages immediately knows what the inner dict holds without unwinding three brace levels first.
Dict.fromkeys vs comprehension, and when to avoid comprehensions
Skip the dictionary comprehension when the value needs a mutable default, when building logic needs a lambda function or a side effect, or when nesting pushes past two levels. Those are the three tells that a comprehension is doing too much.
`dict.fromkeys(iterable, value)` is a built-in class method that builds a new dictionary from a sequence of keys, assigning the same value to every one of them. It beats a comprehension for one specific job: assigning the same starting value to every key. dict.fromkeys(['a', 'b', 'c'], 0) reads clearer than {k: 0 for k in ['a', 'b', 'c']} and skips the loop overhead entirely.
fields = ['name', 'email', 'phone']
# dict.fromkeys: same static value for every key
defaults = dict.fromkeys(fields, None)
print(defaults)
# {'name': None, 'email': None, 'phone': None}
Watch the mutable default trap, though. dict.fromkeys(keys, []) does not give each key its own list, every key points to the same list object, so mutating one mutates all of them:
buckets = dict.fromkeys(['a', 'b'], [])
buckets['a'].append(1)
print(buckets)
# {'a': [1], 'b': [1]} — both keys share one list
safe_buckets = {k: [] for k in ['a', 'b']}
safe_buckets['a'].append(1)
print(safe_buckets)
# {'a': [1], 'b': []} — each key owns its own list
A comprehension like {k: [] for k in keys} avoids this because each iteration creates a fresh list.
Reach for collections.defaultdict instead of either when you're grouping or accumulating values across multiple passes, comprehensions build a dictionary in one shot and can't easily append to a key that already exists.
| Tool | Best for | Avoid when |
|---|---|---|
| Dictionary comprehension | One-shot transform from an iterable | Value is mutable, or logic needs a lambda function |
dict.fromkeys |
Same static value for every key | Value must be mutable per key |
| collections.defaultdict | Grouping, accumulating values | You need a plain dict for output typing |
| for loop | Multi-step logic, side effects, exceptions | Never a bad default, just less compact |
Per PEP 8, lines should stay under 79 characters, a comprehension that wraps three times has already failed that check and should become a loop.
Performance: Comprehension vs for-loop vs dict
Dictionary comprehensions beat a for-loop when you build a new dictionary, but the gap is a constant-factor difference, not a change in time complexity. All three approaches, comprehension, explicit for-loop, and dict fed by zip or a generator expression, are O(n). What changes is how much interpreter overhead sits on top of that n.
We ran a controlled timeit comparison on CPython 3.11.13, building a 100,000-key dictionary three ways: a comprehension, a for-loop with direct key assignment (d[k] = v), and dict(zip(keys, values)). Taking the best of seven runs of ten iterations each, dict comprehensions completed in roughly 3,330 microseconds, the for-loop took roughly 3,650 microseconds, and dict(zip(...)) finished fastest at roughly 2,230 microseconds.
dict(zip(...)) wins here because it skips the comprehension's per-item bytecode dispatch entirely, the C-level zip iterator feeds pairs straight into dict()'s constructor. Treat those figures as a starting point, not gospel: re-run the benchmark against your own CPython build before citing it in a production decision, since minor version bumps and hardware can shift the numbers.
The comprehension still beats the explicit for-loop because CPython compiles it to a dedicated code object using a BUILD_MAP/MAP_ADD bytecode path, skipping the repeated attribute lookups and bytecode dispatch a for loop pays on every iteration. It just doesn't beat dict(zip(...)), which sidesteps per-item Python bytecode entirely by pushing the pairing into C.
| Approach | Time complexity | Memory pattern |
|---|---|---|
| Dict comprehension | O(n) | Builds full dict eagerly |
| For-loop + assignment | O(n) | Same, plus loop overhead |
dict + generator expression |
O(n) | Lower peak memory, lazy source |
Memory footprint is the more interesting trade-off, and it is the one worth weighing before you reach for the fastest syntax. A dict comprehension materializes every key-value pair immediately. If you use dictionary comprehension to create a dict from a large in-memory list, you pay the full memory cost up front.
Feeding dict a generator expression instead defers evaluation. Let's say you're accessing records from a slow I/O source: latency, not CPU, becomes the bottleneck, and lazy construction avoids holding every result in memory at once.
That's the true trade-off: speed versus peak memory, not speed versus correctness. On our own Python services at Netguru, we default to comprehensions for read-heavy, in-memory dict construction and reach for the generator variant only when the source is a lazy iterator we don't want fully realized.
FAQ: Dictionary comprehension quick answers
What is dictionary comprehension in Python?
{k: v for k, v in items} replaces a multi-line for-loop with an explicit dict[key] = value assignment. Use it whenever the transformation is a single expression, per PEP 274.How do you create a dictionary from two lists?
zip() inside a dictionary comprehension: {k: v for k, v in zip(keys, values)}. zip() stops at the shorter iterable, so mismatched lengths silently drop data. Check lengths first if the lists come from separate sources like a CSV read.How do you swap keys and values in a dictionary?
{v: k for k, v in d.items()}, relying on dict.items() to unpack each key-value pair. This only works cleanly if the original values are hashable and unique. Duplicate values will overwrite each other, silently losing keys.When should you use dict.fromkeys() instead of a comprehension?
dict.fromkeys(keys, value) when every key needs the exact same starting value and no per-key logic is involved, it's a single built-in call instead of a loop. Switch to a comprehension the moment the value needs to differ per key, or when the value is mutable (a list or dict), since dict.fromkeys shares one object across every key instead of creating a fresh one.What are the performance trade-offs between the three approaches?
dict(zip(...)) is the fastest for a straight pairing because it does the work in C; a comprehension is next, and still faster than an explicit for-loop because it compiles to a dedicated bytecode path. Once you add a condition, transformation, or mutable per-key default, dict(zip(...)) stops being an option, and the choice becomes comprehension versus for-loop versus dict.fromkeys, where readability matters more than the microsecond-level gap.How do you filter items from an existing dictionary?
{k: v for k, v in d.items() if v > threshold}. This filters items without touching the source dictionary, unlike dict.fromkeys, which sets one value for every key. It is the standard pattern for a discount lookup that drops zero-value entries.Is dictionary comprehension faster than a for-loop?
dict(zip(...)) was faster than both for a straight pairing with no per-item transformation. The gap narrows as the body logic gets heavier, so profile before optimizing.Can you use the walrus operator in a dictionary comprehension?
{k: y for k, v in d.items() if (y:= transform(v)) > 0} avoids calling transform twice. Use it when a filter condition and the output value need the same computed value.-873149-edited.jpg?length=54)