List, Dict, Set and Generator Comprehensions in Python

Written by Brendon
24 June 2025

This guide breaks down list, dict, set, and generator comprehensions — with examples, performance tips, and when not to use them.

Spaceship engine room where list comprehensions are used to power the ship’s core reactor.

Introduction #

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.


List Comprehension #

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:

  • Clarity: For simple transformations, the intent is immediately obvious.
  • Fewer lines of code: That’s not always a goal, but it does help reduce boilerplate.
  • They’re faster: Not dramatically, but Python’s internal optimization makes comprehensions slightly more efficient than an equivalent loop.

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.

if Statements in Comprehensions #

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.

if-else in Comprehensions #

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:

  • Filter with if after the for
  • Transform with 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:

  • Categorizing or labeling items
  • Replacing outliers or nulls with fallback values
  • Creating binary flags or status indicators

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.

Multiple Conditions in Comprehensions #

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.

Function Calls in Comprehensions #

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.

The Walrus Operator (:=) in Comprehensions #

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:

  • You want to avoid duplicate function calls,
  • The logic is short,
  • And the variable name improves clarity.

If you catch yourself nesting walruses like Russian dolls, it might be time to step away from the keyboard and reconsider your life choices.

Nested Comprehensions #

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:

  • Has more than 2 for clauses
  • Or multiple if conditions and nested loops
  • Or you have to mentally simulate the loop just to understand it

…it might be time to switch to regular loops.

Set Comprehension #

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:

  • You want distinct results
  • You don’t care about preserving order
  • You need set operations later (like intersections, unions, etc).

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:

  • Maintain insertion order
  • Preserve duplicates
  • Or rely on indexing

…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 Comprehension #

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:

  1. Duplicate keys — if your comprehension generates the same key more than once, only the last value will be kept:
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.

  1. Too Much Logic — just like nested list comprehensions, a dictionary comprehension can turn ugly if it’s trying to do too much. If your line starts to stretch past 80 characters and needs comments to be understood... maybe break it out into a loop.

Generator Comprehension #

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:

  • You’re working with large datasets.
  • You’re streaming data or reading from files.
  • You want to delay computation until it's needed.
  • You don’t need to index, slice, or reuse the data multiple times.

When Not to Use It

  • If you plan to loop through the values multiple times, a generator won't help — it gets exhausted after one use.
  • If you need random access, like squares[5], you’re out of luck. Generators don’t support indexing.
  • If you need all the data at once, just use a list comprehension — the performance gain from laziness disappears when you immediately force evaluation.

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?

Use Cases, Misuses, and Anti-Patterns #

✅ When Comprehensions Shine

  • Filtering and transforming data: Turning one sequence into another with a clear purpose.
clean_names = [name.strip().title() for name in names if name]
  • Condensing simple loops: One-liners that would otherwise take four or five lines of boilerplate.
  • Creating lookups: Dictionary comprehensions are great for mapping things quickly.
{user.id: user for user in users}
  • Generating sets of unique things: Use a set comprehension when duplicates aren’t welcome.
  • Streaming large data lazily: Generator comprehensions are perfect for huge data where memory matters.

❌ 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:

  • Too many nested loops:
# 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.

  • Complex branching logic: If you're stuffing multiple if/else clauses into a single comprehension, consider switching to a plain loop for clarity.
  • Side effects inside comprehensions: Printing, writing to files, or modifying external state from inside a comprehension? Nope. That’s not what they’re for.
[print(x) for x in range(10)]  # Just use a regular loop.
  • Trying to look clever: If you're writing a comprehension just to look smart — well, bad news: you don’t. You look cryptic.

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.

Performance Comparison: Comprehensions vs Loops #

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

  • List comprehensions are generally faster than manual for loops.
  • Generator comprehensions are fastest to create and best for memory usage.
  • For very simple tasks, comprehensions usually win.
  • For complex logic or multiple branches, performance gains disappear — and readability often tanks.

Wrapping It All Up #

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:

  • Build lists, sets, and dictionaries in a single expressive line
  • Filter and transform data with if and if-else logic
  • Work with nested loops and even nested comprehensions (when you're feeling brave)
  • Use them with functions, generator expressions, and even the walrus operator
  • Gain performance wins compared to traditional loops
  • And we’ve explored where they shouldn’t be used, in the name of readability and sanity

But 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?”