The earlier article on list comprehensions briefly touched on their dict and set counterparts — this one goes deeper into python dictionary comprehension and python set comprehension syntax specifically, including the bugs that show up when the two get confused with each other, or when a filtering condition ends up in the wrong spot.
Introduction: Extending Comprehension Syntax to Dicts and Sets
List comprehension syntax extends naturally to two other collection types, using the same curly braces you already know from dictionary and set literals.
Dict comprehension syntax
{key_expr: value_expr for item in iterable}The canonical first example — numbers mapped to their squares:
squares = {n: n ** 2 for n in range(5)}
print(squares) # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}Set comprehension syntax
{expr for item in iterable}unique_lengths = {len(word) for word in ["cat", "dog", "fish", "ox"]}
print(unique_lengths) # {3, 4, 2}Notice the structural difference between the two: a dict comprehension has a colon separating a key expression from a value expression; a set comprehension has just a single expression, no colon at all. Same braces, genuinely different meaning — a distinction worth internalizing early, since it's easy to miss at a glance.
Dictionary Comprehensions in Practice
Building dicts from zip()
zip() pairs up corresponding items from two sequences, and combining it with a dict comprehension is a clean way to build a dictionary from two related lists:
states = ["California", "Texas", "New York"]
capitals = ["Sacramento", "Austin", "Albany"]
state_capitals = {state: capital for state, capital in zip(states, capitals)}
print(state_capitals)
# {'California': 'Sacramento', 'Texas': 'Austin', 'New York': 'Albany'}Building dicts from .items()
Transforming an existing dictionary — say, applying some operation to every value — typically means iterating its .items():
prices = {"apple": 1.50, "banana": 0.75, "cherry": 4.00}
discounted = {item: round(price * 0.9, 2) for item, price in prices.items()}
print(discounted)
# {'apple': 1.35, 'banana': 0.68, 'cherry': 3.6}Filtering vs. conditional values — a common source of bugs
This distinction was covered for list comprehensions in the earlier article, and it applies identically here, with the same potential for confusion:
Filtering with a trailing if — excludes items entirely from the result:
prices = {"apple": 1.50, "banana": 0.75, "cherry": 4.00}
expensive_ {item: price for item, price in prices.items() if price > 1.00}
print(expensive_only) # {'apple': 1.5, 'cherry': 4.0} — banana is gone entirelyA conditional value expression — keeps every item, but changes the value based on a condition:
labeled = {item: ("expensive" if price > 1.00 else "cheap") for item, price in prices.items()}
print(labeled)
# {'apple': 'expensive', 'banana': 'cheap', 'cherry': 'expensive'} — every item still presentMixing these up is a genuinely common bug: writing a filtering if when you meant a conditional value (accidentally dropping items you wanted to keep, just relabeled), or vice versa (keeping items you meant to exclude). Whenever you're debugging a comprehension that has the wrong number of items in the result, this distinction is the first thing worth checking.
Practical pattern: inverting a dictionary
original = {"a": 1, "b": 2, "c": 3}
inverted = {value: key for key, value in original.items()}
print(inverted) # {1: 'a', 2: 'b', 3: 'c'}The caveat: this only produces a correct, complete result if the original dictionary's values are themselves unique and hashable. If two keys share the same value, the inverted dictionary will silently lose one of them — whichever key was processed last simply overwrites the earlier one at that shared value's position, with no warning.
Practical pattern: building a lookup table from records
users = [
{"id": 101, "name": "Alex"},
{"id": 102, "name": "Sam"},
{"id": 103, "name": "Jordan"},
]
lookup = {user["id"]: user["name"] for user in users}
print(lookup) # {101: 'Alex', 102: 'Sam', 103: 'Jordan'}This is an extremely common real-world pattern — converting a list of records (the kind of shape you'd get back from a database query or an API response) into a fast, ID-keyed lookup dictionary.
Practical pattern: removing sensitive keys from a payload
user_data = {"username": "alex99", "email": "[email protected]", "password": "secret123", "age": 30}
sensitive_keys = {"password"}
safe_data = {key: value for key, value in user_data.items() if key not in sensitive_keys}
print(safe_data)
# {'username': 'alex99', 'email': '[email protected]', 'age': 30}A genuinely practical use case — stripping sensitive fields before logging or displaying a dictionary that came from user input or a database record.
Set Comprehensions in Practice
Core use case: unique values from a sequence
words = ["apple", "banana", "avocado", "cherry", "blueberry"]
first_letters = {word[0] for word in words}
print(first_letters) # {'a', 'b', 'c'}dice_rolls = [3, 5, 2, 3, 6, 5, 1, 3]
unique_rolls = {roll for roll in dice_rolls}
print(unique_rolls) # {1, 2, 3, 5, 6}Both examples follow the same shape: transform (or simply pass through) every item from a sequence, and let the set's automatic deduplication collapse any repeats.
Combining with a filter condition
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
unique_even_squares = {n ** 2 for n in numbers if n % 2 == 0}
print(unique_even_squares) # {64, 4, 36, 100, 16}Contrast with dict comprehensions
Because both dict and set comprehensions use the exact same curly-brace syntax, they're easy to confuse at a glance — the only structural difference is the presence or absence of a colon:
# Dict comprehension — has a colon
{n: n ** 2 for n in range(5)}
# Set comprehension — no colon
{n ** 2 for n in range(5)}When skimming code quickly, it's worth deliberately checking for that colon before assuming which type of comprehension you're looking at — a habit that pays off the first time you're debugging something and initially misread one for the other.
Nested Comprehensions and Common Pitfalls
Shallow nesting for grid-like structures
A nested dict comprehension can build a grid-shaped structure in a single expression — a multiplication table is the classic example:
multiplicati {
i: {j: i * j for j in range(1, 6)}
for i in range(1, 6)
}
print(multiplication_table[3]) # {1: 3, 2: 6, 3: 9, 4: 12, 5: 15}This builds a dictionary of dictionaries — for each i, an inner dict comprehension builds all its multiples. This one level of nesting is generally the practical limit worth reaching for directly; anything deeper tends to become genuinely difficult to read at a glance, and is usually better expressed as a plain nested loop with clear, named intermediate variables.
Common bug: duplicate keys silently overwriting
records = [("apple", 1), ("banana", 2), ("apple", 3)]
result = {name: count for name, count in records}
print(result) # {'apple': 3, 'banana': 2} — the first "apple" entry is just... goneThere's no error here, no warning — the second "apple" entry simply overwrote the first, since dictionary keys must be unique. If your source data might contain duplicate keys and you actually need to keep every value, a dict comprehension is the wrong tool — you'd want to build a dict of lists instead, typically using .setdefault() in a regular loop, as covered in the earlier dictionaries article.
Common bug: {} is not an empty set
empty = {}
print(type(empty)) # <class 'dict'> — not a set!
empty_set = set() # this is the correct way to create an empty setThis is the same gotcha covered in the earlier sets article, but worth repeating here specifically because it also affects comprehensions — if a set comprehension's iterable happens to be empty, the comprehension itself still correctly produces an empty set (since the {...} here has comprehension syntax inside it, not a bare literal), but writing {} directly, with nothing else inside, always means an empty dict.
Common bug: if in the wrong position, quietly changing behavior
As covered in Section 2, mixing up a filtering if with a conditional-value if/else doesn't raise an error — it just quietly produces a differently-shaped result than intended, which makes it a genuinely sneaky class of bug to track down after the fact.
Debugging tip
If a dict comprehension's result has fewer entries than you expected, compare the length of your source data against the number of unique keys it would actually produce:
records = [("apple", 1), ("banana", 2), ("apple", 3)]
print(len(records)) # 3
print(len({name for name, _ in records})) # 2 — fewer unique keys than source records!If those two numbers don't match, duplicate keys are silently collapsing entries in your comprehension — exactly the scenario from the earlier example.
When to Use Comprehensions vs. a Loop
Good fit
Comprehensions genuinely shine for transformations and filters that read clearly in one or two lines: discounting a dictionary of prices, grouping words by their first letter, deduplicating a list of values into a set. In all of these, the logic is simple enough that the comprehension is arguably more readable than the equivalent loop, not less.
Reach for a regular loop instead when...
The logic needs multiple distinct steps that don't collapse cleanly into one expression.
You need error handling (
try/except) around individual items.Nesting would go beyond a single level, as covered above.
You're relying on side effects (printing, writing to a file, modifying something outside the comprehension) rather than actually building a new collection.
Every comprehension has an equivalent loop
Worth remembering as a general principle: comprehensions are a readability and conciseness tool, not a fundamentally different capability from a loop. Anything you can write as a dict or set comprehension, you can also write as a regular loop building up the same result — the comprehension is just a more compact way to express the same logic, when that logic is simple enough to fit cleanly into one line. If it stops fitting cleanly, there's no penalty for falling back to the loop version — it's not a worse solution, just a more explicit one.