1.2.35 · D5Introduction to Programming (Python)

Question bank — Dictionary and set comprehensions

1,671 words8 min readBack to topic

Before you start, three words we'll lean on constantly:

  • iterable — anything you can walk through item by item (a range, a list, d.items()).
  • hashable — a value Python can turn into a fixed fingerprint so it can be a dict key or a set element. See Hashing and hashable types.
  • ternary — the A if cond else B one-line chooser. See Ternary conditional expression.

True or false — justify

Every item below is a statement. Decide true/false and say why before revealing.

{} creates an empty set.
False. {} is an empty dict; dicts claimed the braces first historically. For an empty set you must write set().
{x for x in range(3)} and {x: x for x in range(3)} produce the same type.
False. The first has no colon so it is a set {0,1,2}; the second has a k:v colon so it is a dict {0:0, 1:1, 2:2}. The colon is the whole difference.
A set comprehension can contain duplicate elements if the source has duplicates.
False. A set never stores duplicates; the source can repeat, but the result collapses equal values to one. That collapsing is the reason you'd pick a set.
In a dict comprehension, two different items can safely map to the same key without losing data.
False. Keys are unique, so the later item silently overwrites the earlier one — you lose the first value. See Dictionaries in Python.
{[1,2]: "a"} and {[1,2] for _ in range(1)} both fail for the same reason.
True. A list is unhashable, and both dict keys and set elements must be hashable. Neither can hold a list. See Hashing and hashable types.
A comprehension always builds and stores the full collection in memory.
True for dict/set/list comprehensions. The lazy cousin that does not materialise is the generator expression with (...).
{v: k for k, v in d.items()} is guaranteed to be a perfect inverse of d.
False. It only inverts cleanly if all the values of d were unique; duplicate values become duplicate keys and collide, losing entries.
The filtering if and the if/else value-chooser are just two spellings of the same thing.
False. A trailing if selects which items enter; an if/else ternary chooses the value for items that already got in. Different jobs, different positions.
Set and dict comprehensions run faster than the equivalent explicit loop mainly because they skip the for statement.
False. They still loop over every item. The speed-up is that the iteration and building run in C internally, avoiding per-item Python bytecode for each .add/d[k]=.

Spot the error

Each line is intended to do the stated job but has a bug or wrong assumption. Name it.

Intent: make an empty set. s = {}
s becomes an empty dict, not a set. Use s = set().
Intent: keep every item grouped by remainder. {x % 3: x for x in range(9)}
Only 3 keys survive (0,1,2) and each holds the last x with that remainder — earlier ones are overwritten. To keep all, build a dict of lists with a loop instead.
Intent: a set of coordinate pairs. {[a, b] for a, b in pts}
Lists are unhashable, so this crashes. Use a tuple: {(a, b) for a, b in pts}.
Intent: pair each name with a flag, only for long names. {name: True for name in names if len(name) > 3 else False}
A trailing if cannot take an else. If you want a flag for every name, move the choice into the value as a ternary: {name: (len(name) > 3) for name in names}. If you want to drop short names, keep the trailing if and remove the else.
Intent: unpack (key, value) from a dict. {v: k for k, v in grades}
Iterating a dict directly yields keys only, not pairs, so the unpack fails. You must call .items(): for k, v in grades.items().
Intent: build a set, one value each. {k: v for k, v in pairs}
The colon makes this a dict, not a set. Drop the :v for a set of keys, or {(k, v) for ...} for a set of pairs.
Intent: dict comprehension over two lists. {a: b for a in xs for b in ys}
This is a nested (Cartesian) loop, so every a gets paired with the last b after overwrites — not a one-to-one zip. Use {a: b for a, b in zip(xs, ys)}.

Why questions

Why does the presence of a colon, and nothing else, decide dict-vs-set?
Braces {} are shared by both, so Python needs one signal inside them; the colon says "this element is a key→value pair," its absence says "this element is a lone value."
Why must dict keys (and set elements) be hashable at all?
Both structures use a hash — a fixed fingerprint of the value — to decide where to store it for near-instant lookup. A value that could change (like a list) would break that stored location, so Python forbids it. See Hashing and hashable types.
Why is a set comprehension the natural tool for "distinct things," not a list comprehension?
A list keeps every item including repeats; a set's defining property is that equal elements collapse to one, so de-duplication happens for free instead of you filtering it. See Sets in Python vs List comprehensions.
Why does the filtering if sit at the end while the value-choosing if/else sits at the front?
The end if gates the loop — it runs before a pair is built, deciding entry. The front ternary runs while building the value, deciding what the accepted item stores.
Why can a comprehension replace a 3–4 line loop with one line without changing the result?
The head of the comprehension is exactly what you were storing (d[k]=v or s.add(v)), and the tail is exactly the loop that drove it — it's the same computation, just folded into one expression.
Why is inverting a dict with {v: k for k, v in d.items()} risky in general but fine for a bijection?
Inversion turns old values into new keys; if two old values were equal they now collide and one is lost. When the mapping is one-to-one (a bijection), values are already unique, so no collisions occur.
Why does {x for x in []} give an empty set, but {} gives an empty dict?
{x for x in []} still contains a comprehension body with no colon, so Python parses it as a set that happens to be empty. Bare {} has no body at all, and the default meaning of empty braces is dict.

Edge cases

What does {n: n for n in range(0)} produce?
An empty dict {}. The iterable is empty, so the body never runs, but the colon still fixes the type as dict.
What is {x % 2 for x in range(100)}?
The set {0, 1}. A hundred items map to only two distinct values, and the set keeps each once — a compact demonstration of collapsing.
Does {True: "a", 1: "b"}-style overwriting show up in comprehensions too?
Yes. True hashes equal to 1 (and False to 0), so {k: k for k in [1, True, 1.0]} yields a single key. Numeric-equal, same-hash values are treated as the same key.
What happens with {s: len(s) for s in ("", "a", "ab")} — is the empty string a valid key?
Yes. "" is a perfectly hashable string, so the result is {"": 0, "a": 1, "ab": 2}. "Empty" is not the same as "missing."
If the same value appears via different keys, e.g. {"x": 5, "y": 5} built by a comprehension, is that allowed?
Fully allowed. Only keys must be unique in a dict; values may repeat freely.
What does a set comprehension over unhashable items like {[i] for i in range(3)} do at runtime?
It raises TypeError: unhashable type: 'list' on the first element — sets refuse unhashable members exactly like dict keys do.
Can a comprehension's condition ever produce zero entries even with a non-empty iterable?
Yes. If the trailing if is false for every item (e.g. {n: n for n in range(5) if n > 10}), you get an empty dict — a valid, common edge result.

Recall One-line self-test

Colon or no colon in {expr for item in it}? ::: No colon → set; add k: v → dict. {} type? ::: dict (empty set is set()). Duplicate keys in a dict comprehension? ::: Last one wins, earlier lost. Where does a filter if live? ::: At the very end, after the for.


Connections

  • Dictionary and set comprehensions — parent topic
  • List comprehensions — the [] sibling that keeps duplicates
  • Dictionaries in Python — why keys are unique
  • Sets in Python — why duplicates vanish
  • Generator expressions — the lazy (...) non-materialising cousin
  • Hashing and hashable types — why keys/elements must be hashable
  • Ternary conditional expression — the front-slot value chooser