C.W.K.
Stream
Lesson 02 of 05 · published

Comprehensions, Generators, and the Right Shape

~18 min · comprehension, generator, idiom, pythonic

Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete

The default for sequence transformation

If you're doing "for each x, produce y" — the default in Python is a comprehension. [x*2 for x in xs] for a list, (x*2 for x in xs) for a generator. The for-loop-then-append pattern is what you reach for when comprehensions don't fit, not the other way around.

map and filter — usually not Pythonic anymore

list(map(lambda x: x*2, xs)) works but reads worse than [x*2 for x in xs]. list(filter(lambda x: x > 0, xs)) reads worse than [x for x in xs if x > 0]. The exception: when the function is already named (list(map(str.upper, words))) — that's competitive.

Generators for "process a sequence once"

If the result of a comprehension is going to be consumed once and never re-iterated, swap brackets for parens: sum(x*x for x in xs). Same logic, no intermediate list, short-circuits where possible. The big win in code that processes large or infinite sequences.

The skill — recognizing the shape

The pythonic skill isn't "use comprehensions everywhere." It's recognizing when a transformation fits one. When the inner logic is two lines or has multiple branches, write a real for-loop or extract a helper function. Pythonic is "match the syntax to the complexity."

Pythonic Way: Default to comprehensions for one-line transformations. Default to generators when feeding into a reducer (sum, any, all, max) or when the source is huge. Reach for explicit loops only when the per-element logic outgrows a single expression.

Code

Comprehension vs loop — same logic·python
# Loop
result = []
for n in range(10):
    if n % 2 == 0:
        result.append(n * n)

# Comprehension — preferred for this shape
result = [n * n for n in range(10) if n % 2 == 0]
print(result)            # [0, 4, 16, 36, 64]
Comprehension vs map/filter·python
nums = [1, 2, 3, 4, 5]

# functional — wordy
result = list(map(lambda x: x * 2, filter(lambda x: x > 2, nums)))
print(result)            # [6, 8, 10]

# comprehension — direct
result = [x * 2 for x in nums if x > 2]
print(result)            # [6, 8, 10]

# But map with a named function — competitive
words = ["alpha", "beta"]
upper_a = list(map(str.upper, words))
upper_b = [w.upper() for w in words]
# Both fine. Slight preference for the comprehension.
Generators inside reducers — efficient·python
# Comprehension — builds a list first
total = sum([x * x for x in range(1_000_000)])

# Generator expression — no list, just streams values
total = sum(x * x for x in range(1_000_000))
# Same answer, much less memory

# Same idiom for any/all/max/min/set
any(x < 0 for x in nums)
max(x * 2 for x in nums)
set(x % 7 for x in range(100))
When NOT to comprehension·python
# When the per-element logic is multi-step
result = []
for user in users:
    addr = lookup_address(user.id)
    if addr.country == "KR":
        if user.is_active:
            result.append({
                "name": user.name,
                "email": user.email,
                "address": addr.format(),
            })
# Cramming this into a comprehension would be unreadable.
# An explicit loop is more pythonic here.

External links

Exercise

Rewrite each of these as a comprehension or generator: (a) result = []; for x in nums: result.append(str(x)) (list comp). (b) Sum of squares of even numbers from 0-99 (generator). (c) Dict of {name: len(name)} for a list of names (dict comp). (d) Set of unique first letters from a list of words (set comp). Then: when the loop body is a 5-line multi-step processing of user dicts, why does an explicit for-loop usually win?

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.