A dictionary comprehension fuses a for-loop and a dict() call into one expression. This guide builds the syntax piece by piece — basic builds, if filters, transforming an existing dict, and nested comprehensions — then compares it honestly to loops and the dict() constructor.
You already know how to build a dictionary with a for loop: make an empty {}, loop over something, assign into it one key at a time. That works everywhere, but Python has a shorter form for the common case — one that a lot of code you’ll read (and eventually write) leans on constantly. Our Python dictionaries guide is the mechanics primer for dictionaries in general — creation, .get(), .setdefault(), iteration. This post is about one specific, powerful piece of syntax that sits on top of all that: the dictionary comprehension.
The syntax itself is short enough to type without thinking — {key: value for item in iterable} — which is exactly why it’s easy to write a comprehension that works but reads like a puzzle. This is where people who’ve used comprehensions for months still hesitate before writing one, because the line between “elegant” and “cryptic” is thinner here than almost anywhere else in Python. This guide builds the mental model first, then works through the basic form, filtering, transforming an existing dictionary, nesting, and — honestly — when a comprehension is not the right tool.
A for loop and a call to dict() are two separate things you assemble by hand: create a container, then loop, then fill it in, one statement at a time. A dictionary comprehension fuses those into one expression. Every comprehension has the same four pieces, three of them required:
for item in iterable part that supplies one item at a time.if condition that skips items that don’t qualify.Read a comprehension right to left starting from the for: “for every item in this iterable, [that passes this filter,] compute this key and this value.” You’re describing the shape of the finished dictionary, not the mechanical steps Python takes to build it — Python still loops internally, exactly once per item, but you never write the loop yourself.
No dataset download needed — dictionary comprehensions are a language feature, not a data-analysis tool, so a small hand-written example teaches the syntax better than an external file would. Imagine a neighborhood tool-lending library. It keeps a static catalog of what it owns, and a running log of who’s currently borrowed what:
catalog = [
{"id": "T1", "name": "cordless drill", "category": "power", "replacement_cost": 129.0},
{"id": "T2", "name": "impact driver", "category": "power", "replacement_cost": 95.0},
{"id": "T3", "name": "pressure washer", "category": "yard", "replacement_cost": 210.0},
{"id": "T4", "name": "hedge trimmer", "category": "yard", "replacement_cost": 89.0},
{"id": "T5", "name": "ladder", "category": "general", "replacement_cost": 65.0},
{"id": "T6", "name": "tile saw", "category": "power", "replacement_cost": 150.0},
]
loans = [
{"tool": "cordless drill", "borrower": "Sanne", "checked_out": "2026-06-20", "due": "2026-06-27"},
{"tool": "ladder", "borrower": "Tomas", "checked_out": "2026-06-22", "due": "2026-06-29"},
{"tool": "pressure washer", "borrower": "Amara", "checked_out": "2026-06-25", "due": "2026-07-02"},
{"tool": "cordless drill", "borrower": "Tomas", "checked_out": "2026-07-01", "due": "2026-07-08"},
{"tool": "hedge trimmer", "borrower": "Sanne", "checked_out": "2026-07-03", "due": "2026-07-10"},
{"tool": "ladder", "borrower": "Amara", "checked_out": "2026-07-05", "due": "2026-07-12"},
]
print(len(catalog), "tools;", len(loans), "loan records")6 tools; 6 loan recordscatalog is a fixed list — six tools, each named once. loans is a chronological log, oldest first, and notice "cordless drill" and "ladder" each appear twice — someone else has borrowed them since the first loan. That repetition is deliberate; it’s going to matter later. (The outputs in this post come from Python 3.11 — everything shown also works on Python 3.9+.)
The simplest comprehension takes a list of records and turns one field into keys and another into values. Say you want a quick “tool name → replacement cost” lookup:
cost_by_tool = {t["name"]: t["replacement_cost"] for t in catalog}
print(cost_by_tool){'cordless drill': 129.0, 'impact driver': 95.0, 'pressure washer': 210.0, 'hedge trimmer': 89.0, 'ladder': 65.0, 'tile saw': 150.0}Compare that to writing it as a loop — same result, four extra lines:
cost_by_tool_loop = {}
for t in catalog:
cost_by_tool_loop[t["name"]] = t["replacement_cost"]
print(cost_by_tool_loop == cost_by_tool)TrueBoth produce an identical dictionary. The full grammar a comprehension supports — chained for clauses, multiple if clauses — goes further than this post needs; the official reference on displays for lists, sets, and dictionaries documents the outer edges if you’re curious how far the syntax stretches. Most real code sticks to the simple one-for, one-if shape you’ll see for the rest of this post.
if ClauseAdd an if after the for and Python skips any item that fails it — the item never makes it into the result at all, not even with a placeholder value. Which power tools cost more than $100 to replace?
power_tools_over_100 = {
t["name"]: t["replacement_cost"]
for t in catalog
if t["category"] == "power" and t["replacement_cost"] > 100
}
print(power_tools_over_100){'cordless drill': 129.0, 'tile saw': 150.0}Only two of the four power tools clear $100 — impact driver at $95 and tile saw’s sibling category checks both failed for the other four items, and they’re simply absent, not None or False. The if clause can be any expression that returns something truthy, including the compound and condition above; there’s no separate syntax for “multiple conditions,” just a longer boolean expression.
So far every comprehension has iterated over a list of records. You can just as easily iterate over an existing dict’s .items(), which is how you transform one dictionary into another. Say the library wants an “insured replacement value” — 20% above the raw cost, to cover taxes and shipping if a tool has to be replaced:
insured_value = {name: round(cost * 1.2, 2) for name, cost in cost_by_tool.items()}
print(insured_value){'cordless drill': 154.8, 'impact driver': 114.0, 'pressure washer': 252.0, 'hedge trimmer': 106.8, 'ladder': 78.0, 'tile saw': 180.0}.items() hands the comprehension a (key, value) pair per iteration, and unpacking it straight into name, cost is what makes “apply a function to every value, keep the same keys” this short. The same pattern flipped — using the old value as the new key — inverts a dictionary:
cost_to_name = {cost: name for name, cost in cost_by_tool.items()}
print(cost_to_name){129.0: 'cordless drill', 95.0: 'impact driver', 210.0: 'pressure washer', 89.0: 'hedge trimmer', 65.0: 'ladder', 150.0: 'tile saw'}This inversion is safe here only because every replacement cost happens to be unique — six distinct tools, six distinct prices. Inverting a dict is only lossless when its original values are unique; if two tools ever cost exactly the same, one of them would silently disappear from cost_to_name. Hold that thought — the gotchas section below shows exactly this kind of collision happening for real, with the loan records.
A comprehension’s value expression can be another comprehension, which is how you build a dict of dicts in one statement. Group the catalog by category, with each category mapping to its own tool→cost dictionary:
categories = sorted({t["category"] for t in catalog})
by_category = {
cat: {t["name"]: t["replacement_cost"] for t in catalog if t["category"] == cat}
for cat in categories
}
print(by_category){'general': {'ladder': 65.0}, 'power': {'cordless drill': 129.0, 'impact driver': 95.0, 'tile saw': 150.0}, 'yard': {'pressure washer': 210.0, 'hedge trimmer': 89.0}}That works, and for a catalog this small it’s genuinely fine. Be honest with yourself about the cost of nesting, though. The outer comprehension re-scans the entire catalog list once per category to build the inner dict — three categories here means catalog gets walked three times instead of once, and every level of nesting adds another sentence you have to hold in your head while reading it. The equivalent two-loop version is longer but reads top to bottom instead of inside-out:
by_category_loop = {}
for cat in categories:
tools_in_cat = {}
for t in catalog:
if t["category"] == cat:
tools_in_cat[t["name"]] = t["replacement_cost"]
by_category_loop[cat] = tools_in_cat
print(by_category_loop == by_category)TrueSame result, one full scan per category either way — the loop isn’t faster here, just plainer. For a catalog of six tools it doesn’t matter which you pick; for a nested comprehension a teammate has to debug six months from now, plainer usually wins.
dict(): Which One Should You Reach For?You’ve now seen all three ways to build the same dictionary. Here they are side by side, starting with the no-filter case — every catalog tool, no conditions:
names = [t["name"] for t in catalog]
costs = [t["replacement_cost"] for t in catalog]
comp_result = {t["name"]: t["replacement_cost"] for t in catalog}
loop_result = {}
for t in catalog:
loop_result[t["name"]] = t["replacement_cost"]
ctor_result = dict(zip(names, costs))
print(comp_result == loop_result == ctor_result)
print(ctor_result)True
{'cordless drill': 129.0, 'impact driver': 95.0, 'pressure washer': 210.0, 'hedge trimmer': 89.0, 'ladder': 65.0, 'tile saw': 150.0}All three agree. When you already have two parallel sequences — or can get one cheaply with zip() — dict(zip(names, costs)) is arguably the most readable option: it says “pair these up and make a dict” almost in English. It stops being pleasant the moment you need a filter, though:
comp_filtered = {t["name"]: t["replacement_cost"] for t in catalog if t["replacement_cost"] > 100}
loop_filtered = {}
for t in catalog:
if t["replacement_cost"] > 100:
loop_filtered[t["name"]] = t["replacement_cost"]
ctor_filtered = dict(
(t["name"], t["replacement_cost"]) for t in catalog if t["replacement_cost"] > 100
)
print(comp_filtered == loop_filtered == ctor_filtered)
print(comp_filtered)True
{'cordless drill': 129.0, 'pressure washer': 210.0, 'tile saw': 150.0}All three still agree, but look at what each one costs to read. The comprehension states the filter inline where you’d expect it. The dict() constructor needs a generator expression of tuples wrapped around the same filter — technically one line, but it’s doing more visual work to say the same thing. The loop is the most verbose and also the most flexible: reach for it the moment your per-item logic needs more than one expression (multiple statements, a try/except, logging as you go).
Readability aside, does one actually run faster? Scaling the same idea up to 200,000 key-value pairs and timing each approach with timeit gives a real, if modest, answer:
import timeit
pairs = [(f"item{i}", i) for i in range(200_000)]
def via_comprehension():
return {k: v for k, v in pairs}
def via_loop():
d = {}
for k, v in pairs:
d[k] = v
return d
def via_dict_constructor():
return dict(pairs)
n, r = 30, 5
comp_time = min(timeit.repeat(via_comprehension, number=n, repeat=r))
loop_time = min(timeit.repeat(via_loop, number=n, repeat=r))
ctor_time = min(timeit.repeat(via_dict_constructor, number=n, repeat=r))
print(f"comprehension: {comp_time:.4f}s for {n} runs (best of {r})")
print(f"manual loop: {loop_time:.4f}s for {n} runs (best of {r})")
print(f"dict(pairs): {ctor_time:.4f}s for {n} runs (best of {r})")
print(f"comprehension was about {loop_time / comp_time:.2f}x faster than the manual loop")
print(f"dict(pairs) was about {loop_time / ctor_time:.2f}x faster than the manual loop")comprehension: 0.5823s for 30 runs (best of 5)
manual loop: 0.5839s for 30 runs (best of 5)
dict(pairs): 0.5038s for 30 runs (best of 5)
comprehension was about 1.00x faster than the manual loop
dict(pairs) was about 1.16x faster than the manual loopThat’s a more honest measurement than a single timeit.timeit() call — min() over several repeats filters out one-off noise from whatever else the machine was doing. And the honest result is: the comprehension and the manual loop run at essentially the same speed. Both still execute one Python-level iteration per item; a comprehension is tidier to read, not fundamentally faster than the loop it replaces. dict(pairs) is the one that’s genuinely quicker, because it never runs a Python-level loop at all — pairing up an existing sequence of tuples happens in C. The exact numbers will vary by machine and Python version, but the shape of the result is the real takeaway: pick based on readability first. Speed is a reason to prefer dict() when you already have pairs sitting in a sequence, not a reason to prefer a comprehension over a plain loop.
A comprehension that’s too dense to read at a glance should have been a loop. Combining a filter, a computed value, and a conditional string all in one expression works, but it stops being an improvement over a loop the moment you have to read it twice to understand it:
dense_summary = {
t["name"]: f"${t['replacement_cost'] * 1.2:.2f} insured, {t['category']} tool"
+ (", CHECK CORD" if t["category"] == "power" and t["replacement_cost"] > 100 else "")
for t in catalog if t["replacement_cost"] > 60
}
print(dense_summary){'cordless drill': '$154.80 insured, power tool, CHECK CORD', 'impact driver': '$114.00 insured, power tool', 'pressure washer': '$252.00 insured, yard tool', 'hedge trimmer': '$106.80 insured, yard tool', 'ladder': '$78.00 insured, general tool', 'tile saw': '$180.00 insured, power tool, CHECK CORD'}Correct, but you had to parse a nested conditional expression inside an f-string inside a dict comprehension to trust that. The loop version is longer and says exactly the same thing, one decision at a time:
dense_summary_loop = {}
for t in catalog:
if t["replacement_cost"] <= 60:
continue
label = f"${t['replacement_cost'] * 1.2:.2f} insured, {t['category']} tool"
if t["category"] == "power" and t["replacement_cost"] > 100:
label += ", CHECK CORD"
dense_summary_loop[t["name"]] = label
print(dense_summary_loop == dense_summary)TrueIf you find yourself splitting a comprehension across several lines just to fit an if/else inside the value expression, that’s usually the signal to stop and write the loop instead.
A comprehension’s loop variable doesn’t leak into the surrounding scope — a plain for loop’s does. This is a real difference from Python 2, and it surprises people coming from ordinary for loops, where the loop variable is still visible (and holds its last value) after the loop ends:
tool = "wrench (mine, not the library's)"
tool_costs = {tool: cost for tool, cost in cost_by_tool.items()}
print(tool)wrench (mine, not the library's)The comprehension’s own tool is local to the comprehension — your outer tool variable is untouched. Do the same thing with an actual for loop, and it’s a different story:
tool = "wrench (mine, not the library's)"
for tool, cost in cost_by_tool.items():
pass
print(tool)tile sawAfter the loop, tool has been overwritten with whatever the loop last iterated to — "tile saw", the last key in cost_by_tool. Comprehensions protect you from this by design; ordinary loops don’t.
Duplicate keys don’t raise an error or warn you — the last value silently wins. Build a “who currently has this tool” dictionary straight from the loan log, and watch what happens to "cordless drill", which was checked out twice:
current_borrower = {loan["tool"]: loan["borrower"] for loan in loans}
print(current_borrower)
drill_events = [l for l in loans if l["tool"] == "cordless drill"]
print(drill_events)
print(current_borrower["cordless drill"]){'cordless drill': 'Tomas', 'ladder': 'Amara', 'pressure washer': 'Amara', 'hedge trimmer': 'Sanne'}
[{'tool': 'cordless drill', 'borrower': 'Sanne', 'checked_out': '2026-06-20', 'due': '2026-06-27'}, {'tool': 'cordless drill', 'borrower': 'Tomas', 'checked_out': '2026-07-01', 'due': '2026-07-08'}]
TomasSanne borrowed the drill first, but the result only remembers Tomas, who borrowed it later — the second matching key simply overwrote the first, with no error and no count that tells you it happened. Here that’s actually correct: since loans is in chronological order, “last value wins” happens to mean “most recent loan wins,” which is exactly the current-borrower lookup you want. But that’s a coincidence of this data being sorted — if your source list isn’t chronological, or if you didn’t intend the overwrite, this is exactly how real bugs sneak in: the dictionary comes out with fewer entries than the list you built it from, and nothing tells you which ones vanished. If you need every match, not just the last one, group into a list per key instead of a single value.
Using a comprehension purely for a side effect — and throwing away the dict it builds — is a misuse of the syntax. It’s tempting to reach for a dict comprehension to “loop and do something” because it’s short, even when you were never going to use the resulting dictionary:
overdue = {"cordless drill": "2026-07-08", "pressure washer": "2026-07-02", "hedge trimmer": "2026-07-10"}
reminder_dict = {
t: print(f"Reminder: {current_borrower[t]} still has the {t}")
for t in overdue
}
print(reminder_dict)Reminder: Tomas still has the cordless drill
Reminder: Amara still has the pressure washer
Reminder: Sanne still has the hedge trimmer
{'cordless drill': None, 'pressure washer': None, 'hedge trimmer': None}The reminders did print — print() still runs as a side effect — but reminder_dict is a useless dictionary of Nones that some later reader will wonder about. print()’s return value was never the point; a plain loop says what’s actually happening:
for t in overdue:
print(f"Reminder: {current_borrower[t]} still has the {t}")Reminder: Tomas still has the cordless drill
Reminder: Amara still has the pressure washer
Reminder: Sanne still has the hedge trimmerSame output, no throwaway dictionary, no reader left wondering why a dict comprehension appears with its result never used again. If you’re not going to keep the dictionary a comprehension builds, you didn’t want a comprehension.
A dictionary comprehension is a for loop and a dict() build fused into one expression — you describe the key, the value, the source, and (optionally) the filter, and Python handles the looping:
{k: v for item in iterable} → the basic build-from-a-list form{k: v for item in iterable if cond} → skip items that don’t qualify{k: f(v) for k, v in existing.items()} → transform an existing dict’s values, or invert it (only safe with unique values)dict(zip(...)) → often the cleanest option when you already have two parallel sequences and no filterIf you want to build on this with the frequency-table and safe-lookup patterns that comprehensions often replace, the List Comprehension lesson in our free Python for Data Analytics course covers the closely related list form in depth, and our post on using dictionaries as lookup tables on a real dataset puts the comprehension-built-lookup pattern from this post to work joining two real CSV files.