Learn What Types of Comprehensions Exist in Python: Absolute Beginners Tutorial

Python has four types of comprehensions: list, dictionary, set, and generator. Each one follows the same core pattern but produces a different kind of object. This tutorial covers all four — syntax, behavior, performance characteristics, and when each is the right choice.

Contents

What Is a Comprehension?

A comprehension is a way to build a new collection from an existing iterable — a list, a range, a string, a dictionary, or anything else Python can loop over. Instead of writing a for loop that appends values into an empty collection, you express the entire operation inside a pair of brackets or braces.

Every comprehension follows the same template:

python
# Generic comprehension shape
[expression for item in iterable]
[expression for item in iterable if condition]

The expression produces the output value. The for item in iterable part drives the loop. The optional if condition acts as a filter. The enclosing brackets or braces determine what type of object you get back.

Prerequisites

You need to know what a for loop is and what a list is. No other prior knowledge is assumed.

List Comprehensions[ ]

A list comprehension produces a list. You wrap the expression in square brackets [ ]. The result is exactly what you would get from a for loop that builds a list with append() — the difference is that a comprehension expresses the whole operation as one statement rather than three or four.

syntax anatomy returns list
bracket expression keyword loop variable iterable condition

The for-loop equivalent

python
# Traditional for loop — builds a list of squares
squares = []
for n in range(6):
    squares.append(n ** 2)

print(squares)  # [0, 1, 4, 9, 16, 25]

Written as a list comprehension

python
squares = [n ** 2 for n in range(6)]
print(squares)  # [0, 1, 4, 9, 16, 25]

Adding a filter condition

Place an if clause after the for clause to keep only items that match a condition.

python
# Keep only even numbers
evens = [n for n in range(10) if n % 2 == 0]
print(evens)  # [0, 2, 4, 6, 8]

# Keep words longer than 3 characters
words = ["hi", "hello", "hey", "howdy"]
long_words = [w for w in words if len(w) > 3]
print(long_words)  # ['hello', 'howdy']
"This PEP describes a proposed syntactical extension to Python, list comprehensions." — PEP 202, Python Software Foundation
code builder click a token to place it

Build the list comprehension that collects the length of each word in words:

your code will appear here...
for w [ append(len(w)) len(w) words] in
Why: The correct form is [len(w) for w in words]. The expression len(w) goes first (inside the opening bracket), then for w in words]. append(len(w)) is a method call used in loop-based code, not inside a comprehension.
Scoping: Loop Variables Do Not Exist Outside the Comprehension

In Python 3, the loop variable inside a comprehension exists only within that comprehension. Accessing it afterward raises a NameError. This is a deliberate fix from Python 2, where list comprehension variables leaked into the surrounding scope and could silently overwrite existing variables. Generator expressions, set comprehensions, and dict comprehensions never leaked in Python 2 either — only list comprehensions had this issue, which Python 3 corrected for consistency. Source: PEP 289; Conservative Python 3 Porting Guide.

# Python 3 — loop variable does NOT exist outside the comprehension
squares = [n ** 2 for n in range(6)]
# print(n)  # NameError: name 'n' is not defined

Dictionary Comprehensions{k: v}

A dictionary comprehension produces a dict. You use curly braces { } and write a key: value expression separated by a colon. Dictionary comprehensions were proposed in PEP 274 and became part of Python in versions 2.7 and 3.0.

syntax anatomy returns dict
bracket expression colon separator keyword loop variable iterable
python
# Map each word to its length
words = ["apple", "fig", "banana"]
word_lengths = {w: len(w) for w in words}
print(word_lengths)
# {'apple': 5, 'fig': 3, 'banana': 6}

You can filter with if here too. The condition goes after the for clause, exactly like in a list comprehension.

python
# Invert a dict — swap keys and values
original = {"a": 1, "b": 2, "c": 3}
inverted = {v: k for k, v in original.items()}
print(inverted)
# {1: 'a', 2: 'b', 3: 'c'}

# Keep only items where the value is greater than 1
filtered = {k: v for k, v in original.items() if v > 1}
print(filtered)
# {'b': 2, 'c': 3}
The Colon Is What Makes It a Dict

A dictionary comprehension requires a colon between the key expression and the value expression: {key: value for ...}. Without the colon, Python reads it as a set comprehension, not a dict. The two look nearly identical in structure — the colon is the only distinguishing character.

spot the bug click the line that contains the bug

This code tries to build a dictionary mapping each number to its square, keeping only even numbers. One line is wrong.

1 numbers = range(10)
2 result = {n, n ** 2 for n in numbers if n % 2 == 0}
3 print(result)
The fix: Change {n, n ** 2 ...} to {n: n ** 2 ...}. A dictionary comprehension requires a colon between the key and value expressions, not a comma. A comma would make Python attempt to create a set of tuples, which is not valid syntax here.

Set Comprehensions{ }

A set comprehension produces a set. It looks like a dictionary comprehension but contains only one expression — no colon. Because the result is a set, duplicate values are automatically removed and the order is not guaranteed.

syntax anatomy returns set
bracket expression (no colon — that's what makes it a set, not a dict) iterable
python
# Collect unique first letters from a list of words
words = ["apple", "avocado", "banana", "blueberry", "apricot"]
first_letters = {w[0] for w in words}
print(first_letters)
# {'a', 'b'}  — duplicates removed automatically

# Lowercase all tags, removing duplicates
tags = ["Python", "python", "PYTHON", "code", "Code"]
clean_tags = {t.lower() for t in tags}
print(clean_tags)
# {'python', 'code'}
Empty Braces Create a Dict, Not a Set

Writing {} in Python produces an empty dictionary. To create an empty set, use set(). This is a known gotcha — the syntax is asymmetric by design, because dict literals predated set literals in the language.

Generator Expressions( )

A generator expression produces a generator object. It uses parentheses ( ) instead of brackets or braces. Unlike a list comprehension, it does not compute all values upfront. It produces values one at a time, only when the next one is requested. For large sequences this can eliminate the memory cost of storing the entire result set. Generator expressions were introduced in PEP 289, accepted for Python 2.4.

syntax anatomy returns generator
parentheses (not brackets) expression — computed lazily, one value at a time iterable
python
# List comprehension — all values in memory at once
squares_list = [n ** 2 for n in range(1_000_000)]   # ~8 MB (list of pointers) + object overhead

# Generator expression — computes values on demand
squares_gen = (n ** 2 for n in range(1_000_000))     # tiny

# You can iterate a generator just like a list
for val in squares_gen:
    pass  # values are produced one at a time

# Common use: pass a generator directly to a function
total = sum(n ** 2 for n in range(10))
print(total)  # 285

When you pass a generator expression directly to a function that accepts an iterable — like sum(), max(), or any() — the extra pair of parentheses can be dropped. The function call's parentheses serve double duty.

When to Use Each

Use a generator expression when you only need to iterate once, or when the sequence is large enough that storing all of it in memory at once is a problem. Use a list comprehension when you need to index into the results, check the length, or iterate more than once.

Under the Hood

These are the mechanics that introductory material usually skips. Understanding them will change how you read and write comprehensions.

How CPython actually executes a list comprehension

When CPython compiles a list comprehension, it does not call list.append() in a loop. The compiler generates a dedicated bytecode instruction called LIST_APPEND, which pushes a value directly onto a list object held on the evaluation stack — bypassing the full Python attribute lookup for append. The bytecode path is shorter and involves fewer Python-level operations, which is why list comprehensions measurably outperform manually written append loops on large inputs.

You can inspect this yourself. Run import dis; dis.dis("[x*2 for x in range(5)]") in a Python session and look for the LIST_APPEND instruction in the output. The equivalent for sets is SET_ADD, and for dicts it is MAP_ADD. Generator expressions emit YIELD_VALUE instead, which is how lazy evaluation is wired into the bytecode.

python
import dis

# Inspect the bytecode of a list comprehension
dis.dis("[x * 2 for x in range(5)]")

# Key instruction to look for in the output:
#   LIST_APPEND  — pushes the expression result directly onto the list
#   (no attribute lookup for .append, no method call overhead)

# For comparison, dis a manual loop:
dis.dis("""
result = []
for x in range(5):
    result.append(x * 2)
""")
# Notice LOAD_ATTR for 'append' and CALL — extra overhead every iteration

The walrus operator inside comprehensions (Python 3.8+)

Python 3.8 introduced the walrus operator :=, also called the assignment expression. You can use it inside a comprehension to compute a value once and use it in both the filter condition and the output expression — without computing it twice.

Python Language Reference — Walrus Operator Scoping

According to the Python Language Reference (Python Software Foundation), when a walrus operator assignment appears inside a comprehension, the target variable is bound in the enclosing scope rather than the comprehension's own local scope — the opposite of how ordinary comprehension loop variables behave.

This matters when the expression inside the comprehension is expensive — a function call, a regex match, a database lookup. Without := you must either compute it twice or restructure the whole thing as a loop.

python
import math

# Without walrus: math.sqrt() is called twice for each item
results_bad = [math.sqrt(n) for n in range(20) if math.sqrt(n) > 3]

# With walrus: math.sqrt() is called once per item, result reused
results_good = [root for n in range(20) if (root := math.sqrt(n)) > 3]

print(results_good)
# [3.1622..., 3.3166..., 3.4641..., ...] — values > 3, computed only once each

# Note: the walrus target (root) leaks into the enclosing scope — intentionally.
# This is the ONE exception to Python 3's comprehension scoping rule.
print(root)  # accessible here — last value assigned by := inside the comprehension
The One Scoping Exception

The walrus operator := is deliberately designed to assign into the enclosing scope, not the comprehension's local scope. This means the variable does survive after the comprehension finishes — the exact opposite of the normal comprehension scoping rule. Use this consciously; it is not a bug.

Building a dict from two lists with zip()

A common dict comprehension pattern is building a mapping from two parallel lists. The built-in zip() function pairs items from two iterables, and a dict comprehension turns those pairs into key-value entries. When you also need to transform the keys or values — not just pair them — the comprehension form is the right tool; dict(zip(...)) cannot do that in one expression.

python
keys   = ["host", "port", "timeout"]
values = ["localhost", 8080, 30]

config = {k: v for k, v in zip(keys, values)}
print(config)
# {'host': 'localhost', 'port': 8080, 'timeout': 30}

# dict() with zip achieves the same thing and is slightly more concise
config_alt = dict(zip(keys, values))

# The comprehension form wins when you need to transform keys or values on the way in
config_upper = {k.upper(): v for k, v in zip(keys, values)}
# {'HOST': 'localhost', 'PORT': 8080, 'TIMEOUT': 30}

How generator expressions implement the iterator protocol

Every generator expression returns an object that implements Python's iterator protocol: it has both __iter__() and __next__() methods. Calling __next__() runs the generator's bytecode until the next YIELD_VALUE instruction, then suspends execution and returns the value. The generator remembers its position between calls — the local variables, the loop counter, the current frame — all held in a suspended frame object on the heap. This is why generators are memory-efficient: only one value is in play at any time, and the rest of the sequence has not been computed yet.

python
gen = (x ** 2 for x in range(5))

# A generator IS its own iterator
print(gen is iter(gen))   # True

# Manually step through it with next()
print(next(gen))   # 0
print(next(gen))   # 1
print(next(gen))   # 4

# The for loop calls next() for you until StopIteration is raised
for val in gen:
    print(val)   # 9, then 16 — picks up where next() left off

# Exhausted generators raise StopIteration on the next next() call
# next(gen)  # StopIteration — the generator is spent
Generators Are Single-Pass

A generator expression can only be iterated once. Once it raises StopIteration, it is spent — subsequent calls to next() or further loops over it produce nothing. If you need to iterate results more than once, convert to a list first: results = list(gen_expr).

Common Questions

The following questions address specific behaviors that come up when writing real comprehensions. Each one has a concrete answer.

Can you use a comprehension with a string?

Yes. A string is an iterable of characters, so any comprehension can loop over one directly. This works anywhere you would otherwise iterate character by character in a loop.

python
# List comprehension over a string
chars = [c.upper() for c in "hello"]
print(chars)   # ['H', 'E', 'L', 'L', 'O']

# Filter only alphabetic characters from messy input
clean = [c for c in "h3ll0 w0rld!" if c.isalpha()]
print(clean)   # ['h', 'l', 'l', 'w', 'r', 'l', 'd']

# Set comprehension — collect unique characters (order not guaranteed)
unique = {c for c in "mississippi"}
print(unique)  # {'m', 'i', 's', 'p'}

Can the expression call a function?

Yes. The output expression in a comprehension can be any valid Python expression — including a function call. This makes comprehensions useful for applying the same transformation to every item in a collection without writing out a full loop body.

python
import math

# Call a built-in function as the expression
roots = [math.sqrt(n) for n in range(1, 6)]
print(roots)
# [1.0, 1.414..., 1.732..., 2.0, 2.236...]

# Call a custom function
def grade(score):
    return "pass" if score >= 60 else "fail"

scores = [72, 45, 88, 55, 91]
results = [grade(s) for s in scores]
print(results)
# ['pass', 'fail', 'pass', 'fail', 'pass']

What happens when the iterable is empty?

A comprehension over an empty iterable returns an empty collection of the matching type. The loop simply never executes — no error, no special case.

python
items = []

print([x * 2 for x in items])      # []
print({x: x ** 2 for x in items})  # {}
print({x for x in items})           # set()

gen = (x for x in items)
print(list(gen))                       # []  — generator is immediately exhausted

What is the difference between an if filter and an if-else expression?

An if placed after the for clause is a filter — it controls which items from the iterable are processed at all. An if-else expression placed inside the output expression is a conditional transform — every item passes through, but its output value branches based on the condition. Position determines meaning.

python
numbers = [-3, -1, 0, 2, 5]

# if AFTER for — filter: keeps only positive numbers, shorter list
positives = [n for n in numbers if n > 0]
print(positives)  # [2, 5]

# if-else INSIDE expression — transform: same length, every item is mapped
clamped = [n if n > 0 else 0 for n in numbers]
print(clamped)    # [0, 0, 0, 2, 5]

# You can combine both: transform positives, keep all, clamp negatives to zero
# (the if-else is part of the expression; the trailing if is still a filter)
# Example: clamp negatives, then filter out zeros
result = [n if n > 0 else 0 for n in numbers if n >= 0]
print(result)     # [0, 2, 5]  — negatives removed by filter, 0 passed through transform
Position Determines Meaning

if before the for is part of a ternary expression and requires an else. if after the for is a filter and cannot have an else. Mixing these up is a SyntaxError — and one of the most common mistakes beginners make with comprehensions.

spot the bug click the line that contains the bug

This code intends to replace all negative numbers with zero while keeping positive numbers unchanged. One line contains an error.

1 numbers = [-3, -1, 0, 4, 7]
2 result = [n for n in numbers if n < 0 else 0]
3 print(result)
The fix: The if-else ternary must appear before the for clause, as part of the output expression: [n if n >= 0 else 0 for n in numbers]. An if placed after the for clause is a filter — it cannot have an else branch. This is a SyntaxError.

Can you nest comprehensions?

A nested comprehension uses more than one for clause. The clauses are read left to right, in the same order as the equivalent nested for loops would be written. The most common use is flattening a list of lists into a single list.

python
# Equivalent nested for loops
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

flat_loop = []
for row in matrix:
    for x in row:
        flat_loop.append(x)

# The same thing as a nested list comprehension
# Read the for clauses left-to-right: outer loop first, inner loop second
flat = [x for row in matrix for x in row]
print(flat)   # [1, 2, 3, 4, 5, 6, 7, 8, 9]

# Adding a filter: keep only even values while flattening
evens = [x for row in matrix for x in row if x % 2 == 0]
print(evens)  # [2, 4, 6, 8]
Stop at Two For Clauses

Two for clauses is usually the practical limit. Three or more become difficult to read quickly, and the clause order — which must match the nesting order of equivalent explicit loops — is easy to get wrong. When the logic requires deep nesting, write explicit loops.

What happens with duplicate keys in a dict comprehension?

When a dict comprehension produces the same key more than once, the last value assigned to that key silently overwrites all earlier ones. No error is raised. This is the same behavior as a regular dict literal with repeated keys, and it can cause silent data loss when the source data contains duplicates.

python
# Input has repeated names — only the last score per name survives
records = [("alice", 88), ("bob", 74), ("alice", 95)]
scores = {name: score for name, score in records}
print(scores)
# {'alice': 95, 'bob': 74}   — alice's first score (88) is silently lost

# If you need to preserve all values, collect them into a list instead
from collections import defaultdict
all_scores = defaultdict(list)
for name, score in records:
    all_scores[name].append(score)
print(dict(all_scores))
# {'alice': [88, 95], 'bob': [74]}
Silent Data Loss

Duplicate key overwriting produces no warning. If you are building a dict from data that may contain repeated keys and you need to retain all values, a dict comprehension is the wrong tool. Use collections.defaultdict(list) or itertools.groupby instead.

How to Write Each Type (Step by Step)

All four comprehension types share the same skeleton. The only structural change between them is the outer delimiter — square brackets, curly braces with a colon, curly braces without a colon, or parentheses. The steps below build each type from a common starting point.

Step 1 — Start with a list comprehension

python
# [expression for item in iterable]
doubled = [x * 2 for x in range(5)]
# [0, 2, 4, 6, 8]

Step 2 — Add an optional filter

python
# [expression for item in iterable if condition]
doubled_evens = [x * 2 for x in range(10) if x % 2 == 0]
# [0, 4, 8, 12, 16]

Step 3 — Change brackets to curly braces + colon for a dict

python
# {key_expr: value_expr for item in iterable}
doubled_dict = {x: x * 2 for x in range(5)}
# {0: 0, 1: 2, 2: 4, 3: 6, 4: 8}

Step 4 — Change brackets to curly braces (no colon) for a set

python
# {expression for item in iterable}
doubled_set = {x * 2 for x in range(5)}
# {0, 2, 4, 6, 8}  — a set (unordered, unique values)

Step 5 — Change brackets to parentheses for a generator

python
# (expression for item in iterable)
doubled_gen = (x * 2 for x in range(5))
print(list(doubled_gen))
# [0, 2, 4, 6, 8]  — generator consumed into a list for display

Comparison: All Four Types

The diagram and accordion below compare the four types side by side. Use them as a reference when deciding which form to write.

LIST [ expr ] Produces: list ordered, indexed mutable allows duplicates EAGER all in memory now DICTIONARY {k: v} Produces: dict key-value pairs unique keys only colon is required EAGER all in memory now SET { expr } Produces: set unordered no duplicates no colon EAGER all in memory now GENERATOR ( expr ) Produces: generator lazy evaluation one value at a time single-pass only LAZY computed on demand All four share this core syntax — only the outer brackets change bracket expression for item in iterable if condition bracket [ ] { } ( ) output value loop var source optional filter

List, dict, and set comprehensions are eager — all values computed immediately. Generator expressions are lazy — values computed on demand.

Syntax
[expr for item in iterable]
Returns
A list object
Memory
All values computed and stored immediately
Best for
Building a reusable list you need to index, slice, or iterate more than once — for example, collecting filtered API results, producing a sorted list of transformed records, or pre-computing a dataset you will pass to multiple functions. If you need result[0], len(result), or a second loop over the same data, a list comprehension is the right choice over a generator.
Syntax
{key: value for item in iterable}
Returns
A dict object
Memory
All pairs computed and stored immediately
Best for
Transforming, filtering, or inverting an existing dictionary — for example, uppercasing all keys, removing entries below a threshold value, swapping keys and values, or building a lookup table from two parallel lists using zip(). The comprehension form wins over dict() whenever you need to transform keys or values on the way in, not just pair them.
Syntax
{expr for item in iterable}
Returns
A set object
Memory
All values computed and stored; duplicates removed
Best for
Collecting unique values from an iterable that may contain duplicates — for example, extracting the distinct tags from a list of articles, finding all unique error codes in a log, or normalizing a list of case-mixed strings to a deduplicated lowercase set. Also useful for fast membership testing, since in checks on a set run in O(1) average time versus O(n) on a list.
Syntax
(expr for item in iterable)
Returns
A generator object
Memory
Values produced lazily — one at a time as requested
Best for
Large sequences where you only need a single pass — for example, summing a million computed values with sum(), finding the first match with next(), streaming records from a file without loading them all into memory, or feeding a lazy pipeline into another generator. When the sequence fits comfortably in memory and you need it more than once, convert to a list; otherwise keep it as a generator and avoid paying the memory cost at all.

Check Your Understanding

check your understanding question 1 of 5

Frequently Asked Questions

Python has four types of comprehensions: list comprehensions (produce a list, use square brackets), dictionary comprehensions (produce a dict, use curly braces with a key:value expression), set comprehensions (produce a set, use curly braces with a single expression), and generator expressions (produce a generator object, use parentheses). All four share the same core for...in syntax.
A list comprehension evaluates the entire expression immediately and stores all results in memory as a list. A generator expression is lazy: it produces values one at a time only when requested, keeping memory usage low. Use a list comprehension when you need all results at once or need to access items by index. Use a generator expression when processing large sequences or when you only need to iterate once.
Both use curly braces, but a dictionary comprehension contains a colon separating a key expression from a value expression: {k: v for k, v in items}. A set comprehension contains only a single expression with no colon: {x for x in items}. An empty pair of curly braces {} creates an empty dict, not an empty set — use set() to create an empty set.
Yes. All four comprehension types support an optional filtering condition placed after the for clause: [x for x in items if x > 0]. Only items where the condition is True are processed by the expression. You can also place a conditional (ternary) expression inside the output expression to transform values: [x if x > 0 else 0 for x in items].
List comprehensions are generally faster than equivalent for loops that append to a list, because they are optimized at the CPython bytecode level and avoid the repeated overhead of calling list.append(). Generator expressions use even less memory than list comprehensions for large sequences because they produce values lazily. The practical difference matters most at scale.
Use a comprehension when you are building a new collection from an iterable in a single, readable expression. for loops are better when the loop body is complex, has side effects, or spans multiple statements. If parsing the comprehension requires careful thought, a regular loop is often the clearer choice.
Yes — Python 3.8 and later allow the walrus operator := inside a comprehension. It lets you compute a value once and reference it in both the filter condition and the output expression. For example: [root for n in range(20) if (root := math.sqrt(n)) > 3]. Unlike normal comprehension variables, walrus assignments bind into the enclosing scope, not the comprehension's local scope — so the variable persists after the comprehension finishes.
Yes. Strings are iterables in Python — they produce one character per iteration. Any comprehension can loop over a string directly. A list comprehension over "hello" gives a list of individual characters. A set comprehension over a string gives the set of distinct characters, with duplicates removed automatically.
Yes — the output expression can be any valid Python expression, including a built-in function call (len(), str()), a method call (x.strip()), or a call to your own function. This is one of the most practical uses of comprehensions: applying the same transformation to every item in a collection in a single readable line.
They serve different purposes and live in different positions. An if placed after the for clause is a filter: [x for x in items if x > 0] — items that fail the condition are skipped entirely and the output list is shorter. An if-else placed before the for clause is a conditional expression (ternary): [x if x > 0 else 0 for x in items] — every item passes through but its output value branches. Confusing the two positions is one of the most common beginner syntax errors.
Yes. A nested comprehension uses multiple for clauses: [x for row in matrix for x in row]. The clauses are read left to right, matching the order of nested for loops. The most common use is flattening a list of lists. You should avoid deep nesting — three or more for clauses in one comprehension — because readability drops quickly. If parsing the comprehension takes effort, a plain loop is the better choice.
The last value for any repeated key silently overwrites all earlier ones. No error or warning is raised — Python simply keeps the final value encountered during iteration. This matches how regular dict literals handle repeated keys. If your source data may have duplicates and you need to preserve all values, use collections.defaultdict(list) instead of a dict comprehension.
CPython compiles list comprehensions to a LIST_APPEND bytecode instruction that pushes values directly onto the list's internal array, skipping the Python-level attribute lookup and method call that list.append() requires. This shorter bytecode path is measurable at scale. You can inspect it yourself: import dis; dis.dis("[x*2 for x in range(5)]") and look for the LIST_APPEND instruction. The equivalent instructions for sets and dicts are SET_ADD and MAP_ADD.
final exam
Python Comprehensions — Certification Exam
Questions: 10  ·  Pass mark: 80%  ·  Topic: Python Comprehensions

Enter your name to begin. Your name will appear on the certificate of completion if you pass.

Please enter your name to begin.
Question 1 of 10 Score: 0


Sources & Further Reading

All technical claims in this article are verifiable against the following primary sources. Links open in a new tab.