Python Comprehensions and Iteration Tools

AI-assistedDrafted with AI assistance and reviewed before publishing; examples are version-pinned. Verify against official docs when in doubt.

Python 3.13

“Create an empty list, loop, append” compresses into a single line with a comprehension. This chapter covers comprehensions plus the standard library helpers that remove most remaining loops.

Environment

Examples are verified on Python 3.13. itertools ships with the standard library.

List comprehensions

numbers = [1, 2, 3, 4, 5]

squares = [n * n for n in numbers]           # [1, 4, 9, 16, 25]
evens = [n for n in numbers if n % 2 == 0]   # [2, 4]

The condition filters; a conditional expression transforms:

labels = ["even" if n % 2 == 0 else "odd" for n in numbers]
# ['odd', 'even', 'odd', 'even', 'odd']

Nested loops keep the same order as the equivalent for statements, outermost first:

pairs = [(x, y) for x in range(3) for y in range(2)]
# [(0, 0), (0, 1), (1, 0), (1, 1), (2, 0), (2, 1)]

Dict and set comprehensions

names = ["python", "go", "rust"]

lengths = {name: len(name) for name in names}
# {'python': 6, 'go': 2, 'rust': 4}

inverted = {v: k for k, v in lengths.items()}

adults = {name: age for name, age in ages.items() if age >= 18}

unique_lengths = {len(name) for name in names}   # {2, 4, 6}

Inverting a dict silently loses data when values collide; check uniqueness first if the values are not guaranteed distinct.

Generator expressions

Parentheses instead of brackets produce a lazy generator that computes values on demand:

total = sum(n * n for n in range(10_000_000))    # no giant list in memory

A generator can be consumed once. If you need it twice, materialize it into a list or wrap the logic in a function that creates a fresh generator per call.

For a single argument call, the extra parentheses are optional: sum(x for x in xs).

Built-in iteration helpers

for i, name in enumerate(names, start=1):
    print(i, name)

for name, score in zip(names, scores):
    print(name, score)

zip silently truncates to the shorter input. When the lengths must match, pass strict=True (3.10+):

list(zip(names, scores, strict=True))   # raises ValueError on mismatch

Sorting and extremes:

sorted(words, key=len)                    # by length
sorted(words, key=str.lower)              # case-insensitive
sorted(users, key=lambda u: u.age, reverse=True)
max(words, key=len)                       # longest word

Deduplication with order, reversal, joining:

list(dict.fromkeys(items))   # dedupe, keeps first occurrence order
list(reversed(items))        # iterate backwards without copying
", ".join(names)

any / all short-circuit, so they pair well with generators:

any(n > 100 for n in numbers)   # stops at the first match
all(n > 0 for n in numbers)     # stops at the first failure

itertools

from itertools import chain, islice, count, groupby, combinations

list(chain([1, 2], [3, 4]))      # [1, 2, 3, 4]
list(islice(range(100), 5))      # [0, 1, 2, 3, 4], no list built
list(islice(count(10, 2), 3))    # [10, 12, 14], first 3 of an infinite sequence
list(combinations("abc", 2))     # [('a', 'b'), ('a', 'c'), ('b', 'c')]

groupby groups adjacent items with the same key, so sort by that key first:

rows = [("a", 1), ("a", 2), ("b", 3)]

for key, group in groupby(rows, key=lambda r: r[0]):
    print(key, list(group))
# a [('a', 1), ('a', 2)]
# b [('b', 3)]

The group is an iterator tied to the outer loop; consuming it later gives empty results. Convert to a list inside the loop when you need to keep it.

Common mistakes

1. Nesting comprehensions too deeply

Two levels is usually the limit. Beyond that, a named for loop is easier to read and debug.

2. Side effects inside a comprehension

[print(n) for n in numbers]     # builds a useless list of None

Use a plain loop for side effects.

3. Reusing an exhausted generator

gen = (n * n for n in numbers)
list(gen)     # [1, 4, 9, 16, 25]
list(gen)     # []

4. groupby without sorting

Equal keys that are not adjacent produce multiple groups, which looks like data loss.

5. Mixing up sorted and list.sort

sorted() returns a new list; list.sort() sorts in place and returns None.

6. Overusing comprehensions for complex logic

A comprehension with three conditions and a nested function call is harder to review than a five-line loop.

Summary

  • Comprehensions express transform-plus-filter; dict and set versions use the same shape.
  • Generator expressions trade reusability for memory.
  • enumerate, zip(strict=True), and sorted(key=...) cover most iteration needs.
  • itertools provides infinite sequences, slicing, grouping, and combinatorics.

Next: modules, packages, and virtual environments.