Chapter 11 of 24 · Python for Beginners — Complete Free Tutorial

List, Dict and Set Comprehensions

Loops that build a collection are so common that Python gives them their own syntax. A comprehension turns “start empty, loop, conditionally append” into one expression that reads like the definition of the result. This chapter of the Python for Beginners series covers list comprehensions with conditions and nesting, where they stop being readable, the dict and set versions, generator expressions that never build a list at all, and the walrus operator that can simplify the condition half.

List comprehension basics

The shape is [expression for item in iterable] — “give me this for each that”. Add an if clause to filter, and Python only evaluates the expression for items that pass:

squares = [n * n for n in range(1, 6)]
evens = [n for n in range(1, 11) if n % 2 == 0]

print(squares)
print(evens)

Expected output:

[1, 4, 9, 16, 25]
[2, 4, 6, 8, 10]

Read the first line out loud: “n squared, for n in one to five”. The equivalent explicit loop needs five lines and a temporary list — squares = [], then for n in range(1, 6): with squares.append(n * n), then print(squares) — and answers [1, 4, 9, 16, 25] exactly as the comprehension does. The comprehension is not a big typing win, but it says one thing: this list is that transformation. The same idea in JavaScript is map and filter from arrays and array methods.

Nesting, and where readability stops

Comprehensions nest both in the for clauses and in the expression. Flattening a grid takes two for clauses, in the order you would write the loops:

grid = [[row * 3 + col for col in range(1, 4)] for row in range(3)]
flat = [cell for row in grid for cell in row]
print(grid)
print(flat)

Expected output:

[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
[1, 2, 3, 4, 5, 6, 7, 8, 9]

Nesting is a readability cliff. The rule of thumb that keeps code honest: one for plus one if stays in a comprehension; more than that, or an expression with its own condition, becomes an explicit loop. Compare the same filtering written both ways:

users = [{"name": "Ada", "admin": True}, {"name": "Lin", "admin": False}]
admins = [u["name"] for u in users if u["admin"] and len(u["name"]) > 2]

admins = []
for u in users:
    if u["admin"] and len(u["name"]) > 2:
        admins.append(u["name"])
print(admins)

Expected output:

['Ada']

Both are correct at this size; the second will still be correct after six more conditions are bolted on. If you have to squint, split it.

Dict and set comprehensions

The same syntax with different brackets builds the other two collections. A dict comprehension pairs each key with a value; a set comprehension de-duplicates as it goes:

celsius = {"day": 21, "night": 14}
fahrenheit = {k: round(v * 9 / 5 + 32) for k, v in celsius.items()}
print(fahrenheit)
print(sorted({w.lower() for w in ["Red", "red", "BLUE", "blue"]}))

Expected output:

{'day': 70, 'night': 57}
['blue', 'red']

Note the k: v pair before the first for in the dict version, and the sorted() wrapped around the set version — sets have no order to print. Anything you would build with a loop and a lookup, a comprehension can express in one line.

Generator expressions: the same idea, no list

Wrap the comprehension in round brackets and Python produces a generator expression: an object that yields one value at a time instead of building a list. It works anywhere a list of numbers is expected — notably sum() and " ".join():

import sys

numbers = list(range(1_000_001))
gen = (n * n for n in numbers)

print(f"list: {sys.getsizeof(numbers)} bytes")
print(f"generator: {sys.getsizeof(gen)} bytes")
print(sum(n * n for n in range(11)))
print(" ".join(str(n) for n in range(5)))

Expected output:

list: 8000072 bytes
generator: 200 bytes
385
0 1 2 3 4

(Byte counts vary slightly between Python builds; the ratio is the point.) The list holds every element in memory from the moment it is created; the generator holds a cursor and produces each value only when asked. Use a generator when you will consume the values once — summing, joining, checking a condition — and a list when you need to iterate more than once, index into it, or measure it with len(). Chapter 18 builds generators from scratch.

The walrus operator in a condition

Python 3.8 added :=, which assigns and yields a value in the middle of an expression. It earns its place when a value is needed in the condition and again in the body, especially inside comprehensions:

data = {"reading": 73, "target": 60}

if (delta := data["reading"] - data["target"]) > 10:
    print(f"over target by {delta}")
else:
    print(f"within target (delta {delta})")

Expected output:

over target by 13

Without := you would compute data["reading"] - data["target"] twice or introduce a line just for it. The parentheses around the assignment are required inside if. Reach for it only when it removes a duplication — sprinkled through ordinary code it makes expressions harder to parse, not easier.

A worked program: transforming records

Real code transforms lists of dictionaries. Here three views of the same data are produced, followed by the explicit loop that rebuilds the first view line by line:

products = [
    {"name": "keyboard", "price": 49.0, "stock": 12},
    {"name": "mouse", "price": 15.5, "stock": 0},
    {"name": "webcam", "price": 62.0, "stock": 5},
]

names = [p["name"] for p in products]
in_stock = [p["name"] for p in products if p["stock"] > 0]
labels = {p["name"]: f"${p['price']:.2f}" for p in products}

print(names)
print(in_stock)
print(labels)

names = []
for p in products:
    names.append(p["name"])

print(names)

Expected output:

['keyboard', 'mouse', 'webcam']
['keyboard', 'webcam']
{'keyboard': '$49.00', 'mouse': '$15.50', 'webcam': '$62.00'}
['keyboard', 'mouse', 'webcam']

Lines one and four are identical: the comprehension and the explicit loop build the same list, and only the spelling differs. Choose by intent: when the mapping is the point, the comprehension says it in one line; when conditions accumulate, the loop wins. One caution — labels nests quotes inside an f-string, which reads fine but is a good signal that the expression is nearing its limit.

Common mistakes

  • Calling a method that returns None inside a comprehension — [x.append(y) for ...] builds a list of Nones; if you need the mutation, use an explicit loop.
  • Squeezing three conditions into one line — a comprehension you cannot read aloud is slower to maintain than five plain lines.
  • Reusing a generator later — it is exhausted after one pass; convert to a list with list(gen) if you need a second iteration or len().
  • Forgetting the brackets change the type — [...] is a list, {k: v ...} a dict, {x ...} a set, (...) a generator; {x for x in ...} is a set, not a tuple.
  • Adding side effects to a comprehension — printing or writing files inside one is legal but confusing; comprehension bodies should only compute values.

Practice checklist

  1. Rewrite three of your earlier loops as comprehensions and confirm the output is byte-for-byte identical.
  2. Build a list of squares with a condition (only odd numbers), then filter the same data with an explicit loop.
  3. Flatten a 3×3 grid of your own making and print it with sorted() to check every value is present.
  4. Create a dict comprehension from two lists with zip() and print one formatted line per key.
  5. Measure sys.getsizeof() for a million-item list and its generator expression on your machine, and note both figures.

What's next?

Comprehensions assume everything goes well — but input is rarely so polite. Chapter 12 deals with failure directly in Error Handling with try/except.

Quiz: List, Dict and Set Comprehensions

5 questions · 70% to pass

Frequently Asked Questions

Python is a general-purpose language first released in 1991, designed around one idea: code is read far more often than it is written. English keywords and indentation instead of braces make it readable, the standard library covers files, JSON, dates and testing, and pip adds thousands more packages. The same language handles scripts, web APIs, data work and machine learning.

None at all. Python is widely considered the friendliest first language: no declaration keywords, no braces, no build step, and error messages that name the problem. This series begins at your first print() call. If you already read JavaScript or TypeScript you will adjust quickly, but no previous language is assumed anywhere in these 24 chapters.

Python 3 only, any release from 3.12 onwards. Python 2 reached end of life in 2021 and its print "hi" statements and xrange() no longer run. One historical quirk remains: installers ship the interpreter as python3 on macOS and Linux, while Windows usually offers python or the py launcher, and every command in this series works with that one-word swap.

More than you might expect. Everyday automation such as renaming files, cleaning spreadsheets or scraping pages; web backends with Django, Flask or FastAPI; data analysis and machine learning with pandas, NumPy and PyTorch; DevOps tooling, desktop apps and games. Python is also the classic glue language, calling C, Rust or Fortran libraries from short readable scripts.

Pure Python can run 10 to 100 times slower than C, yet that rarely blocks you: heavy numeric work lives in C extensions such as NumPy, which Python coordinates. Dynamic typing does push errors to runtime, surfacing as a TypeError on the line that finally uses the bad value. Type hints checked by mypy move those mistakes back into your editor.

Only the interpreter. Install CPython from python.org, version 3.12 or newer, confirm it with python3 --version, and open a plain text editor; VS Code with the Python extension is the recommendation here. The early chapters use no third-party packages, so there is nothing else to set up until virtual environments and pip arrive.

A dict is a real runtime mapping: keys may be any hashable value rather than only strings, insertion order has been guaranteed since Python 3.7, and a missing key raises KeyError unless you ask with get(). JavaScript objects coerce every key to a string and carry a prototype chain. A TypeScript interface is erased at compile time and describes shape only; TypedDict is Python's checked equivalent.

Reach for a list when items will be added, removed or reordered: it is ordered, mutable and rich with methods. Pick a tuple for fixed records and function returns, because it is immutable and hashable, so it can be a dictionary key. Choose a set for unique values and fast membership checks; it has no order and de-duplicates on creation.

Yes, gradually. Annotate public function signatures first; the interpreter ignores hints at runtime, so a checker such as mypy is what gives them teeth. Add tests the same way: pytest collects any test_*.py file and plain assert statements, and a suite that finishes in a tenth of a second is one you will run after every change. Both habits cost little now and much more later.

JavaScript and TypeScript keep the browser, so anything rendered to the user stays theirs. Python takes the server: Django for full-featured applications, Flask for small ones, FastAPI for typed async APIs where your annotations become automatic documentation. Your TypeScript habits transfer directly, and asyncio mirrors the event loop you already know, just with different keywords.