Skip to content
ExponaBLOG
PYTHON BACKEND DEVELOPMENT · DAY 005 / 100

List, dict and set comprehensions: a step-by-step guide

Concise transformations — when they stay readable.

Expona Admin7 min read

Comprehensions are one of Python's most elegant features, yet they're often either overused in ways that hurt readability or left on the shelf entirely. This guide walks through list, dict and set comprehensions: a step-by-step guide to knowing when they shine and when to step back to a plain loop.

Whether you're filtering user records, building lookup tables, or transforming API responses, comprehensions let us write transformation logic in a single, declarative line. But that power comes with a responsibility: knowing the readability limits before our teammates (or our future selves) start squinting at the code.

One for and one if is the readability limit

The cardinal rule we follow in production is simple: a comprehension should have one loop and zero or one filter. That's it. Once you cross that threshold, the readability curve drops fast.

Here's what works well. A team member reading this immediately understands the intent:

python
emails = [u.email for u in users if u.active]
premium_ids = {u.id for u in users if u.plan == "premium"}

Both lines have one loop and one filter. The cognitive load is minimal. Anyone scanning the code can see: iterate over users, apply one condition, extract one piece of data.

But add a second loop or a complex condition, and we've crossed the line:

python
# Don't do this
emails = [u.email for u in users if u.active and (u.created_at > cutoff or u.verified)]

# And definitely not this
pairs = [(u.id, p.name) for u in users if u.active for p in u.projects if p.status == "live"]

Both of these hurt readability. The first condition is doing too much logic; the second has nested loops. Write these as loops instead:

python
emails = []
for u in users:
    if u.active and (u.created_at > cutoff or u.verified):
        emails.append(u.email)

pairs = []
for u in users:
    if u.active:
        for p in u.projects:
            if p.status == "live":
                pairs.append((u.id, p.name))

It's more verbose, but it's also more scannable. Your team reads it top-to-bottom without backtracking. In a code review or debugging session, that clarity is worth the extra lines.

Use dict comprehensions to index by key

Dict comprehensions shine when you need to build a lookup table—a mapping from one attribute to another. This is one of the highest-value uses we see in production code.

Suppose we're building a user service. The API returns a list of users, but we need to look up users by ID quickly:

python
users = [
    User(id=1, name="Alice", email="alice@example.com"),
    User(id=2, name="Bob", email="bob@example.com"),
    User(id=3, name="Charlie", email="charlie@example.com"),
]

# Build a lookup table with one line
by_id = {u.id: u for u in users}

# Now we can fetch in O(1) time
user = by_id.get(2)  # User(id=2, name="Bob", ...)

Without the comprehension, we'd write a loop:

python
by_id = {}
for u in users:
    by_id[u.id] = u

Both are clear, but the comprehension is concise. We've eliminated the temporary dict and the append call. The intent is obvious: build a mapping.

We often use dict comprehensions to extract specific fields too:

python
# Map user ID to email for a mailing list
email_by_id = {u.id: u.email for u in active_users if u.consent}

# Map order ID to total amount
totals_by_order = {o.id: sum(item.price for item in o.items) for o in orders}

Again, one loop, one condition (or zero). When you find yourself nesting loops inside a dict comprehension, stop and write a function instead.

Generator expressions for large data

List comprehensions are eager—they build the entire list in memory before you use it. When you're working with millions of rows or streaming data, that can be wasteful. Generator expressions defer evaluation until you actually iterate.

The syntax is nearly identical to a list comprehension; just use parentheses instead of brackets:

python
# List comprehension: builds the whole list upfront
emails = [u.email for u in users if u.active]

# Generator expression: builds items on demand
emails = (u.email for u in users if u.active)

Let's say we're processing a large CSV of orders. Reading all lines into memory would exhaust the heap:

python
import csv

# Bad: loads the entire file into memory
orders = [row for row in csv.DictReader(open("orders.csv")) if float(row["amount"]) > 100]

# Good: reads one row at a time
orders = (row for row in csv.DictReader(open("orders.csv")) if float(row["amount"]) > 100)

# Use it lazily
for order in orders:
    process(order)  # Handles one order at a time

Generator expressions also work well with functions that consume iterables—like sum, max, or any:

python
# Count active users without storing a list
count = sum(1 for u in users if u.active)

# Find the highest order total without building a list
highest = max(o.total for o in orders)

# Check if any orders are overdue without storing a list
has_overdue = any(o.due_date < today for o in orders)

In each case, Python evaluates the generator lazily, stops as soon as it has the answer, and never holds the full dataset in memory. This is critical when you're handling data at scale—which you will be if you're building backends for real users.

Fall back to loops for side effects

Comprehensions are for transformations. They take data in, apply a pure function, and return new data. The moment you need a side effect—writing to a database, logging, updating state—you should reach for a plain loop.

Beginners sometimes try to squeeze side effects into comprehensions. Don't:

python
# Avoid this pattern
[send_email(u.email) for u in users if u.active]

This works because send_email returns something (even if it's None), but it's confusing. The reader expects a comprehension to produce a value, not to perform actions.

Write it clearly:

python
for u in users:
    if u.active:
        send_email(u.email)

Same logic, but now the intent is unmistakable: we're iterating and doing something for each item. No one will misread this as a transformation.

The same applies when you're updating records in a database:

python
# Don't do this
[db.update_user(u.id, verified=True) for u in users if u.email_confirmed]

# Do this instead
for u in users:
    if u.email_confirmed:
        db.update_user(u.id, verified=True)

Or when you're building a list and logging each step:

python
# No
results = [log.info(f"Processing {item}") or process(item) for item in items]

# Yes
results = []
for item in items:
    log.info(f"Processing {item}")
    results.append(process(item))

The loop is longer but infinitely more maintainable. Your reviewers will thank you.

Common mistakes

  • Nesting loops and filters: If you're writing multiple for clauses or complex boolean logic, you've passed the readability threshold. Extract to a function or write a loop.
  • Using comprehensions for side effects: Sending emails, logging, or updating state should happen in explicit loops. Comprehensions are for transformations.
  • Forgetting the variable name in dict comprehensions: {u.id: u for u in users} is clear; {u for u in users} is a set, not a dict.
  • Over-optimizing for brevity: A readable three-line loop beats an unreadable one-liner every time. The goal is code that's easy to review and debug, not code golf.
  • Loading huge datasets into lists when generators would work: If you're iterating once and don't need random access, use a generator expression to save memory.

Key takeaways

  • Limit comprehensions to one loop and at most one filter. Anything more should be a loop.
  • Dict comprehensions are ideal for building lookup tables and indexes—use them liberally.
  • Use generator expressions when iterating over large datasets or piping data into functions like sum or any.
  • Never use comprehensions for side effects (sending emails, writing to databases, logging). Use explicit loops instead.
  • Prioritize readability over brevity. Your team spends far more time reading code than writing it.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles