Zum Hauptinhalt springen

Chapter 10 — Comprehensions and Declarative Transformations

1. Opening problem

Imperative transformation:

result = []

for user in users:
if user.active:
result.append(
user.email.lower()
)

Comprehension:

result = [
user.email.lower()
for user in users
if user.active
]

Both are correct. The important question is not which is shorter.

The question is:

Which form makes the transformation easiest to understand and change?

This chapter develops comprehensions as a declarative tool and defines their readability limits.


2. List comprehensions

General structure:

[
expression
for item in iterable
if condition
]

Example:

squares = [
number * number
for number in range(10)
]

Filtering:

even_squares = [
number * number
for number in range(10)
if number % 2 == 0
]

A comprehension creates a new list.

It should normally express one understandable transformation.


3. Evaluation order

Read a comprehension from left to right:

[
transform(item)
for item in items
if valid(item)
]

Operationally:

result = []

for item in items:
if valid(item):
result.append(
transform(item)
)

The filter runs before the expression is evaluated for accepted elements.

This matters when either operation is expensive or has side effects.

Avoid side effects in comprehensions.


4. Conditional expressions

This:

[
"even" if number % 2 == 0 else "odd"
for number in numbers
]

transforms every element.

This:

[
number
for number in numbers
if number % 2 == 0
]

filters elements.

These forms answer different questions:

  • conditional expression: what output should each input produce?
  • trailing if: should this input be included?

5. Set comprehensions

domains = {
email.split("@", 1)[1].lower()
for email in emails
}

A set comprehension:

  • removes duplicates;
  • does not preserve a semantic ordering contract;
  • requires hashable elements.

Use it when uniqueness is part of the intention.


6. Dictionary comprehensions

users_by_id = {
user.user_id: user
for user in users
}

Potential issue: duplicate keys silently overwrite earlier values.

users_by_email = {
user.email.lower(): user
for user in users
}

If duplicates indicate invalid data, validate instead of accepting last-write-wins behaviour.

Safe helper:

def index_unique(
users,
):
result = {}

for user in users:
key = user.email.lower()

if key in result:
raise ValueError(
f"Duplicate email: {key}"
)

result[key] = user

return result

A normal loop is clearer because collision handling is important.


7. Generator expressions

total = sum(
order.total
for order in orders
if order.paid
)

This does not create an intermediate list.

Compare:

paid_totals = [
order.total
for order in orders
if order.paid
]

total = sum(paid_totals)

The list version may be better if:

  • values are reused;
  • debugging requires inspection;
  • multiple aggregations follow;
  • the dataset is small;
  • a meaningful intermediate concept exists.

The generator version is better for single-pass consumption.


8. Nested comprehensions

Matrix flattening:

flattened = [
value
for row in matrix
for value in row
]

Equivalent:

flattened = []

for row in matrix:
for value in row:
flattened.append(value)

The order of for clauses matches the nested loop order.

Nested comprehensions are acceptable when the structure remains obvious.

Do not compress complex business logic into one expression.


9. Cartesian products

pairs = [
(left, right)
for left in left_values
for right in right_values
]

For large inputs, this can produce enormous results.

If only iteration is needed:

from itertools import product

for left, right in product(
left_values,
right_values,
):
...

Always consider output cardinality.

If each input has 10,000 values, the product has 100,000,000 pairs.

A concise comprehension can still be computationally dangerous.


10. Comprehension scope

Comprehension loop variables have their own scope:

number = 100

squares = [
number * number
for number in range(3)
]

print(number)

Output:

100

The outer binding remains unchanged.

However, names used inside the comprehension are looked up from surrounding scopes.

factor = 2

values = [
number * factor
for number in range(3)
]

The current factor is used during eager evaluation.

Generator expressions differ because they evaluate lazily:

factor = 2

values = (
number * factor
for number in range(3)
)

factor = 10

print(list(values))

The generator uses 10 during consumption.


11. Walrus operator

Assignment expressions use :=.

valid_users = [
normalized
for user in users
if (
normalized := normalize(user)
) is not None
]

This avoids calculating normalize(user) twice.

Without it:

valid_users = []

for user in users:
normalized = normalize(user)

if normalized is not None:
valid_users.append(normalized)

The loop may be easier to read.

Use := when it reduces duplication without obscuring control flow.

Avoid clever use in deeply nested expressions.


12. Side effects are a warning sign

Bad:

[
send_email(user)
for user in users
]

This creates a list of return values that probably is not needed.

Use:

for user in users:
send_email(user)

Comprehensions should create collections or lazy values. A loop communicates side effects directly.

Also avoid:

seen = set()

unique = [
item
for item in items
if item not in seen
and not seen.add(item)
]

This trick depends on set.add() returning None. It is compact but obscure.

Use an explicit helper.


13. Readability threshold

A comprehension is usually clear when it has:

  • one main expression;
  • one or two short for clauses;
  • one or two simple filters;
  • no complex exception handling;
  • no mutation;
  • no hidden I/O;
  • no complicated business rules.

Prefer a loop when you need:

  • logging;
  • metrics;
  • comments;
  • several intermediate names;
  • multiple branches;
  • exception handling;
  • early break or continue;
  • mutation of external state;
  • duplicate detection;
  • per-item recovery.

14. map and filter

Comprehension:

normalized = [
normalize(email)
for email in emails
]

Map:

normalized = list(
map(normalize, emails)
)

map is clear when an existing named function is used.

Filter:

active = list(
filter(is_active, users)
)

Comprehension:

active = [
user
for user in users
if is_active(user)
]

Python teams often prefer comprehensions because the data flow is visible without functional nesting.


15. Chained transformations

Hard to read:

result = [
normalize(user.email)
for user in users
if user.active
if user.email is not None
if validate(user.email)
]

Possible staged design:

active_users = (
user
for user in users
if user.active
)

users_with_email = (
user
for user in active_users
if user.email is not None
)

valid_emails = (
user.email
for user in users_with_email
if validate(user.email)
)

result = [
normalize(email)
for email in valid_emails
]

Or use named generator functions.

Stages improve observability and testability when the pipeline is important.


16. Aggregation

Comprehensions and generator expressions integrate well with:

sum(...)
min(...)
max(...)
any(...)
all(...)

Example:

total_revenue = sum(
order.total
for order in orders
if order.status == "paid"
)

Minimum with default:

smallest = min(
(
value
for value in values
if value > 0
),
default=None,
)

Avoid materializing unless needed.


17. Dictionary inversion

inverted = {
value: key
for key, value in mapping.items()
}

This is safe only when values are unique and hashable.

If values repeat, earlier keys are lost.

A grouped inversion:

from collections import defaultdict

grouped = defaultdict(list)

for key, value in mapping.items():
grouped[value].append(key)

A comprehension is not appropriate because accumulation into shared collections is the main operation.


18. TypeScript comparison

TypeScript commonly uses:

const result = users
.filter(user => user.active)
.map(user => user.email.toLowerCase());

Python:

result = [
user.email.lower()
for user in users
if user.active
]

TypeScript method chains are expressive but may allocate intermediate arrays unless optimized by libraries.

Python comprehensions combine transformation and filtering in one construct.

For lazy pipelines, Python uses generator expressions and iterator tools.


19. Production case study: request normalization

Compact but overloaded:

commands = [
CreateUserCommand(
email=payload["email"].strip().lower(),
name=payload["name"].strip(),
)
for payload in payloads
if payload.get("email")
and payload.get("name")
]

Problems:

  • silently skips invalid records;
  • untyped raw values;
  • repeated dictionary access;
  • no error reporting;
  • missing and blank are merged;
  • conversion exceptions appear inside the expression.

Better:

def parse_command(
payload: dict[str, object],
) -> CreateUserCommand:
email = payload.get("email")
name = payload.get("name")

if not isinstance(email, str):
raise ValueError(
"email must be a string"
)

if not isinstance(name, str):
raise ValueError(
"name must be a string"
)

normalized_email = email.strip().lower()
normalized_name = name.strip()

if not normalized_email:
raise ValueError(
"email must not be blank"
)

if not normalized_name:
raise ValueError(
"name must not be blank"
)

return CreateUserCommand(
email=normalized_email,
name=normalized_name,
)


commands = [
parse_command(payload)
for payload in payloads
]

The comprehension remains simple because parsing complexity moved into a named function.


20. Common mistakes

Excessive nesting

Refactor to loops or helpers.

Side effects

Use a loop.

Silent duplicate overwrites

Validate dictionary keys.

Eager materialization

Use a generator expression when only one pass is needed.

Overusing functional nesting

Prefer readability.

Ignoring cardinality

A product can explode in size.

Hiding validation

Move it into a named parser.


21. English vocabulary

TermMeaning
comprehensioncompact collection-building expression
declarativedescribing the result rather than control steps
transformationconverting each item
filteringselecting items
aggregationcombining many values into one
cardinalitynumber of produced elements
materializationcreating the full result in memory
collisionduplicate key conflict
intermediate valuenamed result between stages
side effectexternally visible state change

Useful sentences:

  • “The comprehension expresses a single transformation clearly.”
  • “A normal loop is preferable because duplicate handling matters.”
  • “The generator expression avoids an intermediate list.”
  • “The Cartesian product has dangerous cardinality.”
  • “Validation should be extracted into a named function.”
  • “This expression hides side effects and should be rewritten.”

22. Speaking task

Explain for seven minutes:

When is a comprehension more readable than a loop?

Give at least three examples and two counterexamples.


23. Writing task

Write a 300-word code-review comment for a nested comprehension containing API calls, exception handling through helper functions, and mutation of a shared set.


24. Exercises

Exercise 1

Convert an imperative active-email loop into a list comprehension.

Exercise 2

Build a dictionary from product ID to product and detect duplicates instead of silently overwriting.

Exercise 3

Flatten a three-level nested collection using a generator.

Exercise 4

Calculate whether every non-empty team has at least one administrator.

Exercise 5

Refactor a side-effect comprehension that sends notifications.


25. Complete solutions

Solution 1

active_emails = [
user.email.lower()
for user in users
if user.active
and user.email is not None
]

If validation or logging becomes complex, move it to a helper or loop.

Solution 2

def index_products(products):
result = {}

for product in products:
product_id = product.product_id

if product_id in result:
raise ValueError(
f"Duplicate product ID: "
f"{product_id}"
)

result[product_id] = product

return result

A dictionary comprehension would hide the collision.

Solution 3

from collections.abc import Iterable, Iterator
from typing import TypeVar

T = TypeVar("T")


def flatten_three(
groups: Iterable[
Iterable[
Iterable[T]
]
],
) -> Iterator[T]:
for outer in groups:
for inner in outer:
yield from inner

Solution 4

every_team_valid = all(
bool(team.members)
and any(
member.is_admin
for member in team.members
)
for team in teams
)

If an empty teams list should be invalid:

every_team_valid = (
bool(teams)
and all(
bool(team.members)
and any(
member.is_admin
for member in team.members
)
for team in teams
)
)

Solution 5

Bad:

[
send_notification(user)
for user in users
]

Good:

for user in users:
send_notification(user)

The loop communicates that side effects are the purpose.


26. Chapter checkpoint

You should now be able to explain:

  1. list, set, and dictionary comprehensions;
  2. generator expressions;
  3. filtering versus conditional transformation;
  4. evaluation order;
  5. comprehension scope;
  6. readability limits;
  7. side-effect warnings;
  8. duplicate-key risks;
  9. cardinality;
  10. staged lazy pipelines.