~/tutorials/python-list-comprehension-12-examples-from-beginner-to-senior
§ POST · MAY 13, 2026 v1.0

Python list comprehension: 12 examples from beginner to senior

12 Python list comprehension examples, from basic squares to nested-loop flattening, dict and set comprehensions, with the 3 places you should not use them.
Ryan CallowayStaff contributor
  9 min read

By Ryan Calloway. Updated May 2026.

Comprehensions are the three highest-impact lines of Python syntax. Used right, they collapse a four-line loop into one expression that reads as one English sentence. Used wrong, they are the unreadable nested mess that someone in your code review is about to ask you to rewrite. This post is 12 working examples — three each at beginner, intermediate, advanced, and senior — plus the four cases where a plain for loop is the right tool. Every example runs on Python 3.13.

Quick answer: a list comprehension has the shape [expression for item in iterable if filter]. Use it when you want a new list that is a transformation or filter of an existing iterable. Drop the brackets to (...) for a memory-friendly generator expression. Use a plain for loop when the body has side effects, multiple statements, or a break. The rest of this post is examples.

What you’ll learn

Prerequisites

The shape of a comprehension

[expression for item in iterable]
[expression for item in iterable if filter]
[expression for outer in iter1 for inner in iter2 if filter]
[result if cond else fallback for item in iterable]
{key: value for item in iterable}     # dict
{expression for item in iterable}     # set
(expression for item in iterable)     # generator

Read it left to right as a sentence: “a list of expression for each item in iterable where filter“. If you cannot read your comprehension that way in one breath, it is too long; promote it to a loop.

Beginner: examples 1–3

1. Square the integers from 1 to 10

squares = [x * x for x in range(1, 11)]
# [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

The “hello world” of comprehensions: one expression, one iterable, no filter. Substitute for map(lambda x: x*x, range(1, 11)) with one fewer import and clearer intent.

2. Uppercase the names that are not empty

names = ["ada", "", "grace", None, "margaret"]
upper = [n.upper() for n in names if n]
# ['ADA', 'GRACE', 'MARGARET']

The filter runs before the transform. if n truthiness-checks for both empty strings and None, so n.upper() never gets called on a value that does not have it.

3. Convert a list of strings to integers

raw = ["1", "2", "3", "42"]
nums = [int(s) for s in raw]
# [1, 2, 3, 42]

Same shape as #1, different expression. The pattern that shows up in every CSV-parsing function I have ever written.

Intermediate: examples 4–6

4. Label numbers as ‘even’ or ‘odd’ (conditional expression)

labels = ["even" if n % 2 == 0 else "odd" for n in range(6)]
# ['even', 'odd', 'even', 'odd', 'even', 'odd']

The if/else goes in the expression position (before for). The if-only filter goes after. Easy to confuse on the first read; the test is “does the comprehension produce one item per input?” — if yes, the conditional belongs in the expression.

5. Build a (name, age) lookup from two parallel lists

names = ["Ada", "Grace", "Margaret"]
ages  = [212, 118, 119]
profile = [f"{n} ({a})" for n, a in zip(names, ages, strict=True)]
# ['Ada (212)', 'Grace (118)', 'Margaret (119)']

The strict=True argument to zip (Python 3.10+) raises if the iterables have different lengths. Use it any time you cannot prove they will match — silent truncation is the bug you write a postmortem about.

6. Filter and transform together: lowercase emails of active users

users = [
    {"email": "Ada@example.com", "active": True},
    {"email": "grace@example.com", "active": False},
    {"email": "MARGARET@example.com", "active": True},
]
emails = [u["email"].lower() for u in users if u["active"]]
# ['ada@example.com', 'margaret@example.com']

The if runs first per item; only items that pass get transformed. You can safely call methods that would fail on the filtered-out items.

Advanced: examples 7–9

7. Flatten a 2D list (nested for)

teams = [["Ada", "Grace"], ["Margaret", "Radia"], ["Barbara"]]
flat = [name for team in teams for name in team]
# ['Ada', 'Grace', 'Margaret', 'Radia', 'Barbara']

Two for clauses in left-to-right order: outer loop first, inner loop second. This is where readers stop following the syntax. My rule: any nested for in a comprehension gets a comment on the line above. itertools.chain.from_iterable(teams) is the more explicit alternative — pick whichever your team reads faster.

8. Cartesian product of two iterables

shirts  = ["red", "blue"]
sizes   = ["S", "M", "L"]
catalog = [(s, z) for s in shirts for z in sizes]
# [('red', 'S'), ('red', 'M'), ('red', 'L'),
#  ('blue', 'S'), ('blue', 'M'), ('blue', 'L')]

For two iterables, itertools.product(shirts, sizes) says exactly what it does and avoids the nested for ambiguity. Use the comprehension when you need to transform the pair on the way out.

9. Build a 5×5 identity matrix (nested comprehension)

I = [[1 if i == j else 0 for j in range(5)] for i in range(5)]
# [[1, 0, 0, 0, 0],
#  [0, 1, 0, 0, 0],
#  [0, 0, 1, 0, 0],
#  [0, 0, 0, 1, 0],
#  [0, 0, 0, 0, 1]]

One comprehension per row, wrapped in an outer comprehension. The conditional expression (1 if i == j else 0) puts a 1 on the diagonal. This is the upper bound of acceptable nesting; three levels deep and you are writing puzzle code.

Senior: examples 10–12

10. Index a list of dicts by id (dict comprehension)

rows = [
    {"id": 1, "name": "Ada"},
    {"id": 2, "name": "Grace"},
    {"id": 3, "name": "Margaret"},
]
by_id = {row["id"]: row for row in rows}
# {1: {"id": 1, "name": "Ada"}, 2: {"id": 2, ...}, 3: {"id": 3, ...}}

The cleanest way to convert “list of dicts” into “dict keyed by id” — the shape every API handler ends up needing twice. The Python tutorial’s dictionaries section shows the variant that swaps keys and values.

11. Extract unique email domains (set comprehension)

emails = ["ada@example.com", "grace@example.org", "margaret@example.com"]
domains = {e.split("@", 1)[1] for e in emails}
# {'example.com', 'example.org'}

Curly braces with no : means a set, not a dict. The set dedupes for free in one pass. The split("@", 1) with the limit prevents the rare bug where a local part contains a quoted @.

12. Walrus operator: keep the expensive call once

def first_admin(team):
    return next((u for u in team if u["role"] == "admin"), None)

teams = [...]

# Bad: calls first_admin(t) twice per team
admins = [first_admin(t)["name"] for t in teams if first_admin(t)]

# Good: calls once, reuses with the walrus operator (PEP 572)
admins = [a["name"] for t in teams if (a := first_admin(t)) is not None]

The walrus assigns a value to a name as part of an expression. Inside a comprehension’s if, it captures the result of an expensive lookup so the expression part can reuse it. PEP 572 is the rationale; the original syntax was contentious enough that Guido stepped down as BDFL during the debate. Used for de-duplicating a single function call, the walrus is fine; used to cram three statements onto one line, it is the case study for why you should have written a loop.

Generator expressions: drop the brackets when you stream

Replace [ and ] with ( and ) and you get a generator: lazy, constant memory, one-pass.

# List comprehension: materialized, ~80 MB
squares_list = [x * x for x in range(10_000_000)]

# Generator expression: a few hundred bytes total
squares_gen = (x * x for x in range(10_000_000))
total = sum(squares_gen)

Use the generator form whenever you pipe to sum, min, max, any, all, or "".join. Those consumers read one item at a time; materialising a 10-million-item list first is wasted memory.

has_admin = any(u["role"] == "admin" for u in users)
total     = sum(o["total"] for o in orders)
csv_line  = ",".join(str(v) for v in row)

When the generator is the only argument to a single function call, you can even drop the parentheses: sum(x*x for x in nums) works. Style is a matter of how readable that call is at a glance.

When a plain loop wins

Rewrite to a for loop when any of these is true.

# Bad: comprehension with side effect
results = [send_email(u) for u in users if u["active"]]

# Good: loop, intent is obvious
results = []
for u in users:
    if u["active"]:
        results.append(send_email(u))

Performance on Python 3.13

List comprehensions consistently beat the equivalent for-loop-with-append by a small margin on CPython, mostly because they skip repeated attribute lookup and dispatch. PEP 709 (Python 3.12) inlined comprehensions into their enclosing frame, removing the implicit function call that earlier versions added. The net effect on 3.13 is that simple comprehensions run a noticeable fraction faster than a for-loop with .append, but the gap is small enough that you should never pick one over the other for performance alone.

Time both forms with timeit on your data when it matters:

python -m timeit -s "data = list(range(10_000))" "[x*x for x in data]"
python -m timeit -s "data = list(range(10_000))" "out=[]\nfor x in data: out.append(x*x)"

Common mistakes

FAQ

Is a list comprehension faster than a for loop on Python 3.13?

Usually a touch faster on small to medium lists, mostly noise on very large ones. PEP 709 in 3.12 inlined comprehensions and narrowed the gap further. Time both with timeit on your data; if you cannot measure a difference, pick the form that reads more clearly.

Can I have multiple conditions?

Yes. Either chain with and, or stack if clauses (which behave like and). [x for x in nums if x > 0 if x < 100] is identical to [x for x in nums if x > 0 and x < 100].

What is the difference between a list comprehension and a generator expression?

Square brackets materialise the whole list; round parentheses produce a one-shot iterator. Use the generator whenever you consume the result once or pipe it into sum, any, all, min, max, or join.

Are nested comprehensions a code smell?

Two levels are usually fine if the comprehension reads as one English sentence. Three levels almost always read better as a loop with a helper function. The “tells” are when readers have to count brackets to figure out which for belongs to which expression.

Can I use match/case inside a comprehension?

Not directly — match is a statement, comprehensions are expressions. The workaround is a helper function that returns the matched value, called from the expression position of the comprehension.

Sources and further reading

If your comprehension is building a lookup, the Python dictionary methods guide covers get, setdefault, and the | merge operator. For the validation layer that lives one step downstream of “list of dicts”, see dataclasses vs Pydantic v2.

esc