Module 2 · Core Data Structures ⏱ 18 min

List & Dict Comprehensions

By the end of this lesson you will be able to:
  • Write a list comprehension that transforms each item in a sequence
  • Filter items inside a comprehension with an if clause
  • Build a dictionary in one line with a dict comprehension
  • Recognise when a comprehension hurts readability and prefer a loop

The pattern is so common it deserves a name. You have a sequence, you want a new one, and every item needs the same small change — each price rounded, each name turned into its length, each number squared. Written as a loop, that is three lines every time: make an empty list, loop, append. A comprehension folds those three lines into one expression that reads almost like a sentence.

Read a comprehension left to right: first what each item becomes, then which sequence it draws from.

nums = [1, 2, 3, 4]
squares = [n * n for n in nums]   # [1, 4, 9, 16]
flowchart LR
  I["[1, 2, 3, 4]"] --> C["n * n  for n in nums"]
  C --> O["[1, 4, 9, 16]"]
  style C fill:#3776ab,color:#fff
A comprehension transforms (and can filter) each item into a new list.

The shape, and how to read it

Square brackets build a list; curly braces build a set or a dict. Inside, three parts sit in a fixed order: an output expression on the left (what each item turns into), a for clause in the middle (where the items come from), and an optional if clause at the end (which items to keep).

[n * n for n in nums] turns each n into its square, while the loop pulls every n out of nums. The output expression runs once per surviving item, and the results are collected in order.

Transform every item, keep only some, or build a dict — one expression each.
nums = [1, 2, 3, 4, 5, 6]

squares = [n * n for n in nums]
print(squares)

evens = [n for n in nums if n % 2 == 0]
print(evens)

lengths = {w: len(w) for w in ['cat', 'hi', 'ok']}
print(lengths)

Transform and filter together

Run that example and three things happen. squares transforms every number. evens filters — the output expression is just n, so each item is kept unchanged and only the test decides. lengths builds a dict, mapping each word to a count of its letters.

That middle case is worth pausing on. When you only want to keep some items without changing them, the output expression and the loop variable share the same name. [n for n in nums if n % 2 == 0] looks odd at first because n appears twice, but it means keep n, exactly as it is, whenever n is even.

Filtering: the if clause goes last

Add an if at the end to keep only the items that pass a test. The position matters and it surprises people: the filter sits after the for, not glued to the output expression.

evens = [n for n in nums if n % 2 == 0]   # [2, 4]

Read it as collect n, for each n in nums, but only when n is even.

Do not confuse this filter with a ternary expression. [n if n > 0 else 0 for n in vals] puts the if before the for, because there it chooses the output value for every item rather than dropping any of them.

flowchart LR
  subgraph F["filter: if AFTER for"]
    A1["keeps only items that pass"] --> A2["drops the rest"]
  end
  subgraph T["ternary: if BEFORE for"]
    B1["picks the output value"] --> B2["runs for every item"]
  end
  style A1 fill:#3776ab,color:#fff
  style B1 fill:#b45309,color:#fff
A filter's if sits after the for (it drops items); a ternary's if sits before the for (it shapes each output).
A dict comprehension maps keys to values; a set comprehension drops duplicates.
lengths = {w: len(w) for w in ['cat', 'hi']}
print(lengths)

unique = {n % 3 for n in [1, 4, 7, 10]}
print(unique)

squares_of_evens = [n * n for n in range(6) if n % 2 == 0]
print(squares_of_evens)

Dictionaries and sets, the same way

Swap the square brackets for curly braces and you can build other collections in one line. A dict comprehension writes key: value as the output expression; a set comprehension drops the colon, and the duplicates with it.

The dict above maps each word to its length. The set collects remainders, and because sets cannot hold repeats, four values that all leave remainder 1 collapse to a single entry.

When dict keys collide

A dict cannot hold the same key twice, so a dict comprehension quietly keeps the last value when keys repeat. Mapping the words ['hi', 'ho', 'hi'] to their lengths writes 'hi' once, overwrites it the second time, and the result holds two entries rather than three.

That is occasionally exactly what you want — deduplicating by a key. More often it is a silent bug, because nothing warns you that two source items fought for the same slot. If your keys might repeat and every value matters, a dict is the wrong shape; reach for a list of pairs instead.

flowchart LR
  C["a comprehension"] --> E["unrolls into"]
  E --> S1["make an empty list"]
  E --> S2["loop over the source"]
  E --> S3["append each result"]
  style C fill:#3776ab,color:#fff
A comprehension unrolls into the same four lines: empty list, loop, optional test, append.

The loop it replaces

A comprehension is a plain loop, squashed. Unrolling [n * n for n in nums if n > 0] by hand gives the four lines it stands in for: create an empty list, loop over the source, test the condition, and append the result. Every part of that loop maps onto a clause in the brackets, in the same order.

Keeping that translation in mind is how you read other people's comprehensions, and how you decide whether to write one. If the unrolled loop would be clear, the comprehension probably is too; if it would need comments to explain, prefer the loop.

Exercise

Write evens_between(lo, hi) that returns a list of the even numbers from lo to hi inclusive, using a comprehension.

def evens_between(lo, hi):
    # one comprehension over range(lo, hi + 1)
    pass
Exercise

What does this print? Each number is squared into a new list.

nums = [1, 2, 3, 4]
squares = [n * n for n in nums]
print(squares)
Exercise

Write word_lengths(words) that returns a dict mapping each word to its length, using a dict comprehension. So word_lengths(['cat', 'ok']) gives {'cat': 3, 'ok': 2}.

def word_lengths(words):
    # one dict comprehension
    pass
Exercise

This comprehension is meant to return the squares of only the even numbers in nums, but it squares every number — the filter is missing. Add the if clause in the right place (after the for).

def square_evens(nums):
    return [n * n for n in nums]   # bug: squares every number
Exercise

Write long_word_lengths(words, min_len) that returns a dict mapping each word longer than min_len to its length. This combines a dict comprehension with a filter — copy carefully, because both the colon and the if matter.

def long_word_lengths(words, min_len):
    # {w: len(w) for ... if ...}
    pass

Recap

  • A comprehension is one expression — [output for item in source] — read left to right.
  • Filter with an if placed after the for; use a ternary (x if cond else y) before the for to pick each output value instead.
  • Curly braces make a dict ({k: v for ...}) or a set ({v for ...}); sets throw out duplicates for free.
  • Reach for a comprehension when it reads cleanly, and for a loop when you need side effects or the logic grows long.

Next you will pass functions around as values, where comprehensions often do the heavy lifting in a single readable line.

Checkpoint quiz

Which comprehension produces [0, 1, 4, 9] from range(4)?

What does {n: n*n for n in range(3)} produce?

You want to keep only the positive numbers, unchanged. Which comprehension is correct?

Go deeper — technical resources