List comprehensions are one of Python's most recognizable features. They let you build a new list from an existing sequence in a single readable line, replacing the pattern of creating an empty list and filling it with a loop. This tutorial covers the syntax, how to add conditions, common use cases, and where comprehensions should and should not be used — with working code examples throughout.
If you have written Python for loops that build lists, you have already done everything a list comprehension does — the syntax just packages it differently. This tutorial walks through what they are, how the syntax works, and where they make your code cleaner.
What Is a List Comprehension?
A list comprehension is a shorthand for building a new list. Instead of writing a loop that appends items one by one, you write a single expression inside square brackets that describes the list you want. The official Python documentation describes them as providing "a concise way to create lists." That brevity is the point: the same logic, fewer moving parts.
Here is the classic pattern you might already know — a loop that squares each number in a range:
squares = []
for n in range(5):
squares.append(n ** 2)
print(squares)
# [0, 1, 4, 9, 16]
The list comprehension version produces exactly the same result:
squares = [n ** 2 for n in range(5)]
print(squares)
# [0, 1, 4, 9, 16]
Both versions produce an identical list. The comprehension is not a different operation — it is a more compact way to write the same logic.
The Syntax Explained
A list comprehension has three parts, always written inside square brackets:
[expression for item in iterable]
# ^^^^^^^^^ ^^^^ ^^^^^^^^
# what goes loop what you
# in the list variable loop over
Each part has a specific role:
- Role
- The value that gets added to the new list for each iteration. It can be the loop variable itself, a calculation, a method call, or any valid Python expression.
- Example
n ** 2— squares the loop variable before adding it to the list.
- Role
- The loop clause.
itemis any variable name you choose;iterableis any sequence Python can loop over — a list, range, string, tuple, or similar object. - Example
for n in range(5)— loops over the integers 0, 1, 2, 3, 4.
- Role
- An optional filter. If present, only items for which the condition evaluates to
Trueare passed to the expression and included in the new list. - Example
if n % 2 == 0— keeps only even numbers.
Tip: the filter condition is optional — [x ** 2 for x in range(5)] is also valid.
[x * 2 for x in [1, 2, 3, 4, 5] if x % 2 != 0]Comprehension vs. For Loop
The table below shows the same operation written both ways. The output is always identical — the difference is only in how the code is structured.
- For loop
result = []for w in words:result.append(w.upper())- Comprehension
[w.upper() for w in words]
- For loop
result = []for n in nums:result.append(n * 2)- Comprehension
[n * 2 for n in nums]
- For loop
result = []for s in strings:result.append(len(s))- Comprehension
[len(s) for s in strings]
Adding Conditions to Filter Items
The optional if clause at the end of a comprehension acts as a filter. Items that do not satisfy the condition are skipped and never reach the expression.
# Keep only even numbers from 0 to 9
evens = [x for x in range(10) if x % 2 == 0]
print(evens)
# [0, 2, 4, 6, 8]
# Keep only words longer than 3 characters
words = ["hi", "hello", "bye", "python", "is"]
long_words = [w for w in words if len(w) > 3]
print(long_words)
# ['hello', 'python']
The condition tests each item before the expression runs. If a condition would cause an error (such as calling a method on None), use a condition that checks for that first: [x.upper() for x in items if x is not None].
You can also combine a transformation and a filter in the same comprehension:
# Square only the even numbers
squared_evens = [x ** 2 for x in range(10) if x % 2 == 0]
print(squared_evens)
# [0, 4, 16, 36, 64]
Build a list comprehension that collects only the positive numbers from a list called nums.
[expression for item in iterable if condition]. Start with the expression inside the opening bracket, add the loop clause, then the condition with the closing bracket. The distractor if x < 0] filters for negative numbers, not positive ones.
How to Convert a For Loop
-
Start with a working for loop
Write out the standard pattern: an empty list, a loop, and an
appendcall. Make sure it works correctly before converting it. Example:result = []; for item in collection: result.append(item.lower()). -
Identify the expression and the iterable
Look at what the loop appends — that is your expression. Look at what the loop iterates over — that is your iterable. In the example above, the expression is
item.lower()and the iterable iscollection. -
Write the comprehension inside square brackets
Open a square bracket, write the expression, then write
for item in iterable, then close the bracket:[item.lower() for item in collection]. Assign it to a variable the same way you would the loop result. -
Add an optional condition to filter items
If the loop had an
ifstatement controlling whether to append, add that same condition at the end:[item.lower() for item in collection if item]. Only items that pass the condition are included.
Common Use Cases
The following patterns come up constantly in real Python code. Each one maps directly to the syntax you have already learned — the only thing that changes is what you put in the expression position and whether you add a filter.
Transforming items in a list
names = ["alice", "bob", "carol"]
upper_names = [name.capitalize() for name in names]
print(upper_names)
# ['Alice', 'Bob', 'Carol']
Filtering items from a list
scores = [45, 82, 91, 37, 68, 75]
passing = [s for s in scores if s >= 60]
print(passing)
# [82, 91, 68, 75]
Extracting values from a list of objects
users = [
{"name": "Alice", "active": True},
{"name": "Bob", "active": False},
{"name": "Carol", "active": True},
]
active_names = [u["name"] for u in users if u["active"]]
print(active_names)
# ['Alice', 'Carol']
Working with strings and characters
# Collect only the vowels from a word
word = "comprehension"
vowels = [ch for ch in word if ch in "aeiou"]
print(vowels)
# ['o', 'e', 'e', 'i', 'o']
Calling your own function in the expression
Any valid Python expression works in the expression position — including a call to a function you wrote yourself. This is one of the most practical patterns in real code: define a small helper, then apply it across a list in one line.
def celsius_to_fahrenheit(c):
return (c * 9 / 5) + 32
readings = [0, 20, 37, 100]
converted = [celsius_to_fahrenheit(c) for c in readings]
print(converted)
# [32.0, 68.0, 98.6, 212.0]
# Works with built-in functions too
raw = [" hello ", " world ", " python"]
cleaned = [s.strip() for s in raw]
print(cleaned)
# ['hello', 'world', 'python']
Building a list of tuples or pairs
The expression position is not limited to single values. You can return a tuple from each iteration, which gives you a list of pairs — useful for building lookup tables, coordinate sets, or paired data without zip().
# Each iteration produces a (number, square) tuple
pairs = [(x, x ** 2) for x in range(1, 6)]
print(pairs)
# [(1, 1), (2, 4), (3, 9), (4, 16), (5, 25)]
# Pair each word with its length
words = ["Python", "list", "comprehension"]
word_lengths = [(w, len(w)) for w in words]
print(word_lengths)
# [('Python', 6), ('list', 4), ('comprehension', 13)]
When the expression is a tuple, the parentheses around it are optional inside a comprehension — [(x, x**2) for x in range(5)] and [x, x**2 for x in range(5)] are not the same thing. The second form is a SyntaxError. Always wrap tuple expressions in parentheses to make the intent clear.
What happens when the iterable is empty?
A comprehension over an empty list or range does not crash — it silently returns an empty list. This is the correct and expected behavior, and you can rely on it.
result = [x * 2 for x in []]
print(result)
# []
result = [x for x in range(5) if x > 100]
print(result)
# [] — all items filtered out, still no error
This code should produce a list of lengths for words longer than 4 characters. One line contains a mistake. Click the line you think is wrong, then check your answer.
result on line 2, but line 3 tries to print results with an extra s. Python would raise a NameError: name 'results' is not defined. The fix is to change print(results) to print(result).
Multiple Conditions in a List Comprehension
You can stack more than one condition in a comprehension by chaining if clauses. Python treats them as a logical and — an item must satisfy every condition to be included.
# Two conditions: divisible by 2 AND by 3
nums = range(1, 21)
result = [x for x in nums if x % 2 == 0 if x % 3 == 0]
print(result)
# [6, 12, 18]
# The same logic written with a combined condition using 'and'
result = [x for x in nums if x % 2 == 0 and x % 3 == 0]
print(result)
# [6, 12, 18]
Two chained if clauses and a single if x % 2 == 0 and x % 3 == 0 produce identical results. Most Python style guides favor the single and form for clarity — it makes the logical relationship between conditions explicit.
You can also mix a ternary expression in the expression position with a filter condition at the end:
# Filter out zeroes, then label each remaining number
nums = [0, 3, 0, 7, -2, 5]
result = ["pos" if x > 0 else "neg" for x in nums if x != 0]
print(result)
# ['pos', 'pos', 'neg', 'pos']
Using enumerate() and zip() with List Comprehensions
Any iterable works as the source of a list comprehension — including the built-ins enumerate() and zip(), which let you work with index-value pairs or multiple sequences at once.
enumerate() — looping with an index
enumerate(iterable) yields (index, value) tuples. You can unpack both in the for clause:
fruits = ["apple", "banana", "cherry"]
# Pair each item with its position number
labeled = [f"{i}: {fruit}" for i, fruit in enumerate(fruits)]
print(labeled)
# ['0: apple', '1: banana', '2: cherry']
# Keep only items at even-numbered positions
even_pos = [fruit for i, fruit in enumerate(fruits) if i % 2 == 0]
print(even_pos)
# ['apple', 'cherry']
zip() — looping over two lists in parallel
zip(a, b) pairs items from two iterables position by position. Again, you can unpack both names in the for clause:
names = ["Alice", "Bob", "Carol"]
scores = [88, 74, 95]
# Combine into formatted strings
report = [f"{name}: {score}" for name, score in zip(names, scores)]
print(report)
# ['Alice: 88', 'Bob: 74', 'Carol: 95']
# Filter: only show names where the paired score passes
passing = [name for name, score in zip(names, scores) if score >= 80]
print(passing)
# ['Alice', 'Carol']
This code should use zip() to pair names with scores and return only the names where the score is 70 or above. Something is wrong. Find the line causing the problem.
if scores >= 70 but it should be if score >= 70. scores is the whole list — comparing a list to an integer raises a TypeError. The loop variable unpacked from zip() is score (singular), not scores. The fix: change scores >= 70 to score >= 70.
In Python 3, the loop variable inside a list comprehension is isolated from the surrounding code and does not leak into it. Writing [x for x in range(5)] does not leave a variable x accessible afterward — unlike a regular for loop, which does leave the loop variable in scope after the loop finishes. This isolation was introduced in Python 3 (Python 2 comprehensions did leak). In Python 3.12 and later, the implementation changed from a hidden nested function to inlined bytecode (PEP 709), but the isolation guarantee is unchanged: the LOAD_FAST_AND_CLEAR instruction saves any outer variable with the same name before the comprehension runs, and STORE_FAST restores it afterward.
List Comprehension vs. Generator Expression
Changing the outer square brackets of a list comprehension to parentheses produces a generator expression. The syntax looks almost identical, but the behavior is fundamentally different.
PEP 202, the proposal that introduced list comprehensions in Python 2.0, aimed to give developers a more compact syntax for building lists without the boilerplate of the loop-and-append pattern. That intention — conciseness in service of clarity — remains the guiding principle: a comprehension should not need a comment to explain itself.
# List comprehension — builds the entire list immediately in memory
squares_list = [x ** 2 for x in range(1000000)]
# All one million values exist in memory right now
# Generator expression — produces values one at a time on demand
squares_gen = (x ** 2 for x in range(1000000))
# No values are computed yet; the generator waits to be consumed
| Aspect | List comprehension [ ] |
Generator expression ( ) |
|---|---|---|
| Result type | list |
generator object |
| When values are computed | All at once, immediately | One at a time, on demand (lazy) |
| Memory use | Holds all items in memory | Holds only one item at a time |
| Reusable? | Yes — iterate as many times as you like | No — exhausted after one pass |
| Supports indexing? | Yes — result[0] works |
No — must consume in order |
| Best for | When you need the full list or will iterate more than once | When you only need to pass values one-by-one (e.g. to sum(), max()) |
If you are immediately passing the result to a function that consumes it once — like sum(), max(), or any() — a generator expression is more memory-efficient and equally readable. If you need to index into the result, iterate it multiple times, or pass it to code that expects a list, use a list comprehension.
nums = [1, 2, 3, 4, 5]
# Generator expression passed directly to sum() — no list ever built
total = sum(x ** 2 for x in nums)
print(total)
# 55
# List comprehension needed here — we index into the result
squares = [x ** 2 for x in nums]
print(squares[2])
# 9
Common Mistakes
A few patterns consistently trip up newcomers to list comprehensions.
Using curly braces {} instead of square brackets [] creates a set or dictionary, not a list. Always use [ and ] for list comprehensions.
Putting the condition in the wrong place
# WRONG — condition before the for clause is a ternary, not a filter
result = [x if x > 0 for x in nums] # SyntaxError
# CORRECT — filter condition goes after the for clause
result = [x for x in nums if x > 0]
Confusing filtering with ternary expressions
nums = [-2, 3, -1, 4]
# This filters — produces only positive numbers
positives = [x for x in nums if x > 0]
# [3, 4]
# This uses a ternary — every item is kept, negatives become 0
clipped = [x if x > 0 else 0 for x in nums]
# [0, 3, 0, 4]
A ternary expression (value_if_true if condition else value_if_false) goes in the expression position, before the for clause. A filter (if condition alone) goes after the for clause. They are different constructs that produce different results.
Nesting too deeply
# Hard to read — avoid this
result = [x * y for x in range(3) for y in range(3) if x != y]
# Easier to read as an explicit loop
result = []
for x in range(3):
for y in range(3):
if x != y:
result.append(x * y)
Using or in a condition — it does not work like two separate filters
Chained if clauses act as and, not or. If you want items that match either of two conditions, you must use the or operator inside a single if clause — not two separate if clauses.
nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# CORRECT — use 'or' in a single if clause to keep items matching either condition
result = [x for x in nums if x < 3 or x > 8]
print(result)
# [1, 2, 9, 10]
# WRONG MENTAL MODEL — two chained if clauses act as 'and', not 'or'
# This keeps only items where BOTH conditions are true simultaneously,
# which is impossible for a single integer — produces an empty list
result = [x for x in nums if x < 3 if x > 8]
print(result)
# []
Using a comprehension for side effects instead of building a list
Functions like print(), list.sort(), and file.write() return None. If you call them in the expression position, the comprehension will collect a list of None values — which is almost certainly not what you intended, and it wastes memory building a list you never use. Use a plain for loop when you need side effects.
items = ["apple", "banana", "cherry"]
# WRONG — builds a useless list of None; print() returns None
result = [print(item) for item in items]
print(result)
# apple
# banana
# cherry
# [None, None, None] ← this is the actual list comprehension produced
# CORRECT — use a plain for loop when the goal is side effects, not a new list
for item in items:
print(item)
Under the Hood: What CPython Actually Does
Most Python resources explain what list comprehensions do without explaining how CPython executes them differently from a for loop. The difference comes down to bytecode — and how that bytecode has evolved.
In all Python versions, the key instruction is LIST_APPEND: a dedicated bytecode opcode that adds a value to the list being built. A regular for loop calling result.append(item) must perform a full attribute lookup on the result list object every iteration — Python resolves .append as an attribute, retrieves the bound method, and then calls it. LIST_APPEND skips that attribute resolution entirely, which is the primary reason comprehensions benchmark faster than equivalent loops.
Python 3.12 changed the execution model significantly. Before 3.12, a list comprehension was compiled as a separate code object — essentially a hidden nested function that was created, called once, and immediately discarded. PEP 709, implemented in Python 3.12, changed this: comprehensions are now inlined directly into the surrounding function's bytecode. There is no longer a separate code object, no temporary function object allocation, and no new Python frame pushed onto the stack. The measured result was up to 2x faster in microbenchmarks and approximately 11% faster across real-world benchmark suites that use comprehensions heavily. Variable isolation — the loop variable not leaking into the outer scope — is preserved in 3.12+ through a pair of new instructions (LOAD_FAST_AND_CLEAR and STORE_FAST) that save and restore any outer variable with the same name on the stack.
You can inspect this directly using the dis module, which is part of Python's standard library. In Python 3.12 and later, running dis.dis() on a function containing a comprehension will show the inlined bytecode — including LOAD_FAST_AND_CLEAR, BUILD_LIST, LIST_APPEND, and STORE_FAST — all within the same code object as the surrounding function rather than in a nested one:
import dis
# Disassemble a list comprehension
dis.dis(lambda: [x * 2 for x in range(5)])
# Disassemble an equivalent for loop
def with_loop():
result = []
for x in range(5):
result.append(x * 2)
return result
dis.dis(with_loop)
Running this in Python 3.12+ shows the comprehension inlined into the outer function, using LIST_APPEND directly, while the loop version shows explicit LOAD_ATTR and CALL instructions for result.append. This is not a micro-optimization you should chase — it is simply the explanation for why list comprehensions benchmark faster, and why that speed advantage widened further in Python 3.12.
Before Python 3.12, a list comprehension was compiled as a hidden nested function — a separate code object allocated with MAKE_FUNCTION, called immediately, and then discarded. Every comprehension therefore involved a full Python function-call roundtrip. PEP 709 (Python 3.12) removed that overhead by inlining the comprehension directly into the surrounding function's bytecode. The practical effect: up to 2x faster in microbenchmarks of the comprehension itself, and about 11% faster across real-world benchmark suites that use comprehensions heavily, according to the PEP's reference implementation results. Variable isolation — the loop variable not leaking into surrounding scope — is preserved through the LOAD_FAST_AND_CLEAR and STORE_FAST instructions. If you are on Python 3.11 or earlier, the description in older tutorials (a hidden nested function) was accurate for your version; if you are on 3.12 or later, the comprehension runs inline.
List comprehensions were introduced in PEP 202, authored by Barry Warsaw and shipped in Python 2.0. The PEP described list comprehensions as providing "a more concise way to create lists." The scope behavior — where the loop variable is isolated and does not leak into surrounding code — was introduced in Python 3 as a deliberate improvement over Python 2, where comprehension variables did leak. In Python 3.12, the implementation mechanism changed (from a nested function to inlined bytecode via PEP 709), but the isolation guarantee was preserved. The official reference is the Python 3 documentation on data structures.
Beyond Lists: Dict and Set Comprehensions
Once you are comfortable with list comprehensions, the same syntax extends to two other Python built-in types with only a bracket change. You will encounter both in real Python code immediately after leaving beginner territory, so it is worth knowing they exist.
A dictionary comprehension uses curly braces and a key: value pair as the expression. A set comprehension uses curly braces with a single expression — like a list comprehension but with {} instead of [], which produces a deduplicated, unordered collection.
# List comprehension — square brackets, returns a list
squares_list = [x ** 2 for x in range(5)]
print(squares_list)
# [0, 1, 4, 9, 16]
# Dictionary comprehension — curly braces + key:value, returns a dict
squares_dict = {x: x ** 2 for x in range(5)}
print(squares_dict)
# {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
# Set comprehension — curly braces + single expression, returns a set (no duplicates)
letters = {ch.lower() for ch in "Mississippi"}
print(letters)
# {'m', 'i', 's', 'p'} — order may vary; duplicates removed
[...] — list comprehension, always ordered, allows duplicates. {key: value ...} — dict comprehension, maps keys to values. {expression ...} — set comprehension, deduplicated, unordered. An empty {} with no for clause is a dict literal, not a set. The rules for the for clause, the optional if filter, and the expression position are identical across all three forms — everything you learned about list comprehensions transfers directly.
Key Takeaways
- A list comprehension builds a new list from an existing iterable in a single expression using the form
[expression for item in iterable]. - The three parts are expression, loop clause, and optional filter. The expression defines what goes in the list; the loop clause defines what you loop over; the condition filters which items are included.
- List comprehensions and for loops produce the same result. Use comprehensions for simple, readable transformations and filters; use loops when the logic is complex or requires side effects.
- The condition always goes after the for clause. Placing
if conditionbefore theforkeyword is either a syntax error or a ternary expression — two different things. - Comprehensions are generally faster than equivalent for loops because CPython uses optimized internal instructions for list building rather than the full attribute-lookup overhead of
list.append(). Python 3.12 made them faster still by eliminating hidden function-call overhead (PEP 709). In everyday code, readability matters more than this speed difference. - The loop variable does not leak. In Python 3, the iteration variable inside a comprehension is isolated from the surrounding scope — by design since Python 3.0, and preserved through new bytecode mechanics in Python 3.12+.
- Never use a comprehension for side effects. Calling
print()or any function that returnsNonein the expression position builds a useless list ofNonevalues. Use a plainforloop when the goal is side effects, not a new list. - The same syntax extends to dicts and sets.
{k: v for ...}is a dict comprehension;{expr for ...}is a set comprehension. Every rule you learned here applies to both. - Readability should guide the choice. If a comprehension requires a second read to understand, a regular for loop is the better option.
List comprehensions are most useful when the transformation or filter is simple enough to read at a glance. The goal is clear, expressive code — not the shortest possible line count.