This guide breaks down list, dict, set, and generator comprehensions — with examples, performance tips, and when not to use them.
Before we dive into all the different types of Python comprehensions, let’s take a moment to understand what problem they’re actually solving — and why they’re worth learning in depth.
At their core, comprehensions are just a more compact way to create new collections. Instead of writing a full for-loop to build a list
, set
, or dictionary
, Python gives us a shorthand syntax that handles it in a single, readable line.
You've likely seen this kind of thing before:
squares = [x * x for x in range(10)]
This line creates a list of squares from 0 to 9. It’s simple, expressive, and easy to read — once you know what you’re looking at. But as with many elegant Python features, there's more going on beneath the surface.
Comprehensions can do more than just iterate and transform. They can include conditional logic. They can call functions. They can nest. They can even assign values mid-expression using the walrus operator. And when used well, they make your code faster, cleaner, and easier to reason about.
But used poorly? They can make it almost unreadable.
This post is about understanding both sides of that coin.
We’ll start with the basics — how list comprehensions work and how they differ from regular loops. Then we’ll move into conditionals, function calls, nested structures, sets, dictionaries, and generators. We’ll also talk about when comprehensions are helpful and when they become an anti-pattern, especially in terms of readability and performance.
By the end, you should not only be able to write comprehensions fluently, but also recognize when not to.
Let’s begin with the most common and most approachable: list comprehensions.
By the way, here's the video version of this article. Perfect if you want a short, high level overview before diving into this more in depth article.
Let’s start with the most familiar and widely used type: list comprehensions. If you’ve written any amount of Python, chances are you’ve already seen one — or several — in the wild.
At a high level, list comprehensions are a cleaner, more concise way to create a new list by transforming or filtering items from an existing iterable.
Here’s a simple example:
numbers = [1, 2, 3, 4, 5]
squares = [x * x for x in numbers]
This creates a new list of squares: [1, 4, 9, 16, 25]
.
If we rewrote that using a traditional for
loop, it would look like this:
squares = []
for x in numbers:
squares.append(x * x)
Same result — just more lines of code. With a comprehension, you get the same logic in a single expression. That’s really the core idea: take a loop that builds a list and express it inline.
The general syntax looks like this:
[expression for item in iterable]
You can read it like a sentence: “Give me expression
for each item
in iterable
.”
Why Use Them?
There are a few good reasons to reach for a list comprehension:
That said, clarity only holds as long as the logic stays simple. You’ll see later that things can get messy if you try to cram too much into a single comprehension.
So far, we’ve just transformed each item in a list. But what if you only want some of the items?
That’s where the if
clause comes in.
Python allows you to filter the items from the iterable using a trailing if
statement — right inside the comprehension.
Here’s a classic example: creating a list of only the even numbers from a range.
evens = [x for x in range(10) if x % 2 == 0]
This reads: “Give me x
for each x
in range(10)
, but only if x
is divisible by 2.”
Compare that to the loop version:
evens = []
for x in range(10):
if x % 2 == 0:
evens.append(x)
Again, same result — but the comprehension is cleaner and highlights the filtering logic inline with the transformation.
A Note on Placement
It's important to notice where the if
goes. In this case, the if
is filtering the iterable — it decides whether to include the item at all. It does not provide an alternative value.
We’ll get to that in the next section, when we add full if-else
logic inside comprehensions. But for now, just remember:
[x for x in iterable if condition]
..means “include x
if the condition is true.” Simple filtering.
So far, we’ve filtered items out using if
. But what if you don’t want to exclude anything — you just want to return different values depending on some condition?
This is where the inline if-else
expression comes in.
Here’s a simple example: labeling numbers as 'even'
or 'odd'
.
labels = ['even' if x % 2 == 0 else 'odd' for x in range(5)]
# Result: ['even', 'odd', 'even', 'odd', 'even']
Now we’re not filtering anything out — we’re applying a transformation that depends on a condition.
Notice the structure here:
[<value_if_true> if <condition> else <value_if_false> for item in iterable]
This is a bit different from the previous filtering form. The if
comes before the for
, and there’s an else
to complete the expression.
Let’s compare:
Filtering:
[x for x in range(10) if x % 2 == 0]
Conditional value:
['even' if x % 2 == 0 else 'odd' for x in range(10)]
The takeaway here is:
if
after the for
if-else
before the for
When to Use This
The inline if-else
form is useful for situations where you’re mapping one set of values to another — but with a conditional twist. Some common use cases:
But — and this is important — the more complex the condition, the harder it gets to read. As always, if your logic starts requiring parentheses and deep nesting, it might be a sign to fall back to a plain loop.
At this point, you might be wondering: Can I add more than one condition to a comprehension? And the answer is yes — but with a few caveats.
Let’s say you only want numbers that are both even and greater than 3:
[x for x in range(10) if x % 2 == 0 and x > 3]
# Result: [4, 6, 8]
Nice and clean. Just stack your conditions using and
, or
, or even not
. Python’s full boolean expression syntax is fair game here.
Want to be pickier?
[x for x in range(20) if x % 2 == 0 and x % 3 == 0]
# Result: [0, 6, 12, 18]
You can go as deep as your logic needs — just remember: you’re still in a one-liner.
This is the tipping point. Python lets you cram quite a bit into a comprehension, but readability starts to slide downhill fast when conditions get too dense or cryptic.
What About Chaining Multiple if Clauses?
Some developers think you can do this:
[x for x in range(10) if x > 3 if x % 2 == 0]
And… that actually works.
But this is just syntactic sugar. Python interprets it like:
[x for x in range(10) if x > 3 and x % 2 == 0]
You’re applying one filter, then another — functionally equivalent to combining them with and
. This form is mostly useful when you want to emphasize separate filters, but in practice, most people just use a single if
with combined conditions.
Still, it’s good to know this is valid:
results = [x for x in data if is_valid(x) if has_permission(x)]
Just use it sparingly — clarity beats cleverness.
Now that we’ve seen how to add logic with if
and if-else
, let’s take things up a notch: calling functions inside a comprehension.
This is one of the cleanest ways to keep comprehensions readable while still doing something non-trivial. Rather than stuffing complex expressions inline, you can offload that logic to a helper function — and keep your comprehension short and sweet.
Let’s say you’ve got a list of words, and you want to normalize them (lowercase and strip punctuation):
def clean_word(word):
return word.lower().strip('.,!?')
raw_words = ['Hello', 'world!', 'PYTHON,', 'rocks.']
cleaned = [clean_word(w) for w in raw_words]
# Result: ['hello', 'world', 'python', 'rocks']
See? Much cleaner than doing all that string wrangling inline. The comprehension focuses on what you’re doing (transforming each word), and the function handles the how.
You Can Combine This With Conditions Too
Let’s filter while we’re at it:
def is_keyword(word):
return word in {'if', 'else', 'for', 'while'}
keywords = [w for w in raw_words if is_keyword(w.lower().strip('.,!?'))]
If your inner logic is long, extract it. This is Pythonic. Comprehensions are great — but they’re not an excuse to write unreadable one-liners.
Also, don’t feel guilty about defining tiny functions just to use in a comprehension. Think of them as clarity helpers.
Be Mindful of Performance
Function calls do introduce overhead, especially in tight loops or large datasets. If performance is critical (we’ll get to profiling later), you might want to profile different approaches using timeit
or restructure your logic.
But most of the time, using functions in comprehensions is a win — it reads better, and keeps your code modular.
Ah yes, the walrus operator. Introduced in Python 3.8, it lets you assign a value to a variable as part of an expression — and yes, that includes comprehensions.
The syntax looks like this:
value := expression
It’s called the "walrus" operator because :=
looks like the eyes and tusks of a walrus. Python naming tradition: strong on charm, light on subtlety.
Why Use It?
Let’s say you’re calling a function that’s a little expensive, but you want to both filter by and transform with its result — and you don’t want to call it twice.
Before the walrus:
results = []
for x in data:
val = compute(x)
if val > 10:
results.append(val)
With the walrus:
results = [val for x in data if (val := compute(x)) > 10]
That one-liner reads: "Give me val
(which is compute(x)
) for each x
in data
, but only if val > 10
."
You assign and compare at the same time — saving a line and avoiding a double computation.
Another Example
Let’s say you’re reading and parsing user input:
lines = ["42", "", "17", "error", "100"]
parsed = [num for line in lines if (num := parse_int(line)) is not None]
Here, parse_int()
might return None
on bad input. The comprehension filters out failures, but also keeps the parsed result for reuse — without calling parse_int()
twice.
A Word of Caution
The walrus operator is powerful, but it can also make your code harder to parse — especially for readers who aren’t familiar with the syntax. If you’re sacrificing clarity to shave off a line or two, ask yourself: Is it really worth it?
Python prides itself on readability, and this is one of those features that walks a fine line. It’s best used when:
If you catch yourself nesting walruses like Russian dolls, it might be time to step away from the keyboard and reconsider your life choices.
Sometimes a single loop isn’t enough. Maybe you’re dealing with a list of lists, or you want to flatten data, or transform it across multiple dimensions. That’s where nested comprehensions come in.
Let’s start with a simple case: flattening a 2D list.
matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
]
flattened = [num for row in matrix for num in row]
# Result: [1, 2, 3, 4, 5, 6, 7, 8, 9]
Notice the order: for row in matrix
happens first, then for num in row
. It reads left to right, just like regular for-loops would be nested:
for row in matrix:
for num in row:
...
You Can Add Conditions, Too
Want only the even numbers?
evens = [num for row in matrix for num in row if num % 2 == 0]
# Result: [2, 4, 6, 8]
Again, this is equivalent to:
for row in matrix:
for num in row:
if num % 2 == 0:
...
You’re not inventing new logic — just condensing it.
Practical Use: Cartesian Products
Need every combination of items from two lists? Easy.
colors = ['red', 'blue']
sizes = ['S', 'M', 'L']
combos = [(color, size) for color in colors for size in sizes]
# Result: [('red', 'S'), ('red', 'M'), ('red', 'L'), ('blue', 'S'), ...]
This reads: "For every color, for every size, pair them up." Comprehensions make this kind of data shaping feel effortless.
When It Goes Too Far
Nested comprehensions are powerful — but depth comes with danger. Once you go beyond two levels, readability tanks. If you’re doing something like:
[func(x, y, z) for x in a for y in b for z in c if condition(x, y, z)]
You might want to press pause and ask: Should this really be a comprehension?
There’s a point where it becomes a riddle, not a feature. In those cases, a plain ol’ nested loop is clearer and easier to debug.
Rule of Thumb
If your comprehension:
…it might be time to switch to regular loops.
So far we’ve been building lists. But what if you don’t care about order or duplicates?
Enter: set comprehensions.
They look just like list comprehensions — except they use curly braces {}
instead of square brackets []
.
nums = [1, 2, 2, 3, 4, 4, 5]
unique_squares = {x * x for x in nums}
# Result: {1, 4, 9, 16, 25}
Behind the scenes, this works just like a loop adding items to a set — so duplicates are automatically removed.
Use Cases
Set comprehensions are ideal when:
For example, grabbing all unique first letters from a list of names:
names = ['Alice', 'Bob', 'Charlie', 'Anna', 'Brian']
initials = {name[0] for name in names}
# Result: {'A', 'B', 'C'}
Conditions Work Here Too
Just like list comprehensions, you can filter with if
:
evens = {x for x in range(10) if x % 2 == 0}
# Result: {0, 2, 4, 6, 8}
Behind the Scenes: Hashing
Sets are backed by hash tables, so they require elements to be hashable — that means no lists or dictionaries inside your set comprehension. Tuples are fine (as long as they only contain hashable types):
points = {(x, y) for x in range(3) for y in range(3)}
This gives you a 3x3 grid of coordinate pairs — with no duplicates and fast lookup times.
When Not to Use It
If you need to:
…a set comprehension is the wrong tool. Sets are unordered and unindexed by design.
But when you need uniqueness and speed? Set comprehension is your friend.
Dictionary comprehensions let you build a dictionary in one tight, readable line. The syntax is simple:
{key_expr: value_expr for item in iterable}
Let’s see it in action.
Example: Squaring Numbers into a Dict
nums = [1, 2, 3, 4]
squares = {x: x * x for x in nums}
# Result: {1: 1, 2: 4, 3: 9, 4: 16}
That reads: “For each x
in nums
, create a key x
with value x * x
.”
It’s short. It’s expressive. It’s very Pythonic.
With Conditions
Of course you can filter:
even_squares = {x: x * x for x in nums if x % 2 == 0}
# Result: {2: 4, 4: 16}
And as with the others, you can use function calls or unpack tuples — anything you’d normally do in a loop.
More Real-World-ish Example
Let’s say you have a list of names and want to create a lookup dictionary of name lengths:
names = ["Alice", "Bob", "Charlie"]
length_lookup = {name: len(name) for name in names}
# Result: {'Alice': 5, 'Bob': 3, 'Charlie': 7}
Boom. One line. Clear intent.
Unpacking Tuples in the Loop
Sometimes you’re iterating over key-value pairs already — maybe from a list of tuples or a dictionary:
pairs = [("a", 1), ("b", 2), ("c", 3)]
flipped = {val: key for key, val in pairs}
# Result: {1: 'a', 2: 'b', 3: 'c'}
Or even:
original = {"a": 1, "b": 2}
inverted = {v: k for k, v in original.items()}
# Result: {1: 'a', 2: 'b'}
When to Be Careful
Two warnings here:
nums = [1, 2, 2, 3]
d = {x: x * 2 for x in nums}
# Result: {1: 2, 2: 4, 3: 6}
The second 2 silently overwrites the first. No errors, no warnings.
Generator comprehensions look almost exactly like list comprehensions — the only difference is the parentheses:
squares = (x * x for x in range(10))
That’s it. You’ve made a generator. No brackets, no curly braces — just good old round parentheses.
Want a deeper dive into generators? Check out our complete guide to Python generators.
But unlike lists, this doesn’t actually do the computation upfront. It just sets things up. You can loop through it or convert it to a list later, but it won’t actually calculate anything until you ask for it:
for square in squares:
print(square)
Why Generators?
Because they’re lazy — and that’s a compliment in this context.
They compute values on demand, one at a time, and forget them once they’re done. That means they use very little memory, especially with large data sets:
big = (x for x in range(1_000_000))
# This won't eat up all your RAM like a list would
Need the values later? Wrap it in list()
and you're back in familiar territory:
list_of_squares = list(x * x for x in range(10))
When to Use It
Use generator comprehensions when:
When Not to Use It
squares[5]
, you’re out of luck. Generators don’t support indexing.A Practical Example
Let’s say you’re working with a giant file:
with open("huge_file.txt") as f:
long_lines = (line for line in f if len(line) > 80)
This keeps memory usage low by yielding one line at a time instead of loading the whole file into memory. Neat, right?
✅ When Comprehensions Shine
clean_names = [name.strip().title() for name in names if name]
{user.id: user for user in users}
❌ When Not to Use Them
Here’s the rule of thumb: If you have to pause and squint to understand the comprehension, it’s probably doing too much.
Some red flags:
# What fresh hell is this?
matrix = [[i * j for j in range(5)] for i in range(5)]
It works, but your future self might curse you six months from now.
if/else
clauses into a single comprehension, consider switching to a plain loop for clarity.[print(x) for x in range(10)] # Just use a regular loop.
Readability Counts
Python has a guiding principle called the Zen of Python (try running import this
in a Python shell). One of its most famous lines is:
Readability counts.
Comprehensions follow this philosophy beautifully when used well. They’re meant to make your code more readable, not less.
If your comprehension is becoming a logic puzzle, pull back. Break it into multiple lines. Use helper variables. Comment your intent. There’s no prize for doing it in one line.
Let’s go full lab coat and bring in timeit
, Python’s built-in module for benchmarking tiny snippets of code.
The Setup
We’ll compare a basic task: squaring numbers from 0 to 9999.
List comprehension version:
squares = [x * x for x in range(10_000)]
Traditional for-loop version:
squares = []
for x in range(10_000):
squares.append(x * x)
The test:
Using timeit
in a Python shell:
import timeit
timeit.timeit('[x * x for x in range(10_000)]', number=1000)
# Around: 0.25 seconds
timeit.timeit('''
squares = []
for x in range(10_000):
squares.append(x * x)
''', number=1000)
# Around: 0.27 seconds
Winner: List comprehension
Why? Because list comprehensions are compiled into tighter, more optimized bytecode than equivalent for loops. They avoid function/method calls like .append()
, which can be relatively slow in CPython. This makes them faster.
What About Generator Comprehensions?
Let’s compare:
gen = (x * x for x in range(10_000))
This is even faster to create than a list — but only because it doesn’t actually do any calculations upfront. So if you’re not iterating immediately, it's practically free.
timeit.timeit('(x * x for x in range(10_000))', number=1000)
# Around: 0.00024 seconds
But once you consume the generator:
timeit.timeit('list(x * x for x in range(10_000))', number=1000)
# Around: 0.35 seconds
Still good — but slightly slower than a list comprehension due to function call overhead.
TL;DR
Comprehensions in Python are one of those elegant features that seem deceptively simple — until you realize just how much they can do.
We’ve seen how they let you:
if
and if-else
logicBut the real magic of comprehensions is how they blend power with clarity — when used with care. Python’s motto could just as easily be: “Simple things should be simple. Complex things should at least try to be readable.”
So, next time you find yourself reaching for a for
loop just to transform or filter a collection, ask yourself:
“Can this be a comprehension?”