skip to content

When is a list comprehension the wrong choice in Python, and what do you write instead?

level: middleimportance: should knowfreq 48%

answer

  1. The construct should match the intent
  2. A discarded result means the wrong tool
  3. Expressions cannot hold statements
  4. No try, no break, no continue inside
  5. Name a helper before expanding to a loop

basics

~20 s

Use a plain for loop when you want a side effect rather than a container, when you need statements a comprehension cannot hold such as try/except or break, or when the expression has grown past what a reader can take in at once.

solid answer

~40 s

A comprehension says *build this container by mapping and filtering that one*. Whenever that is not the sentence you mean, write the loop. Three cases dominate. First, a comprehension written for its side effect — `[send(x) for x in items]` — builds and discards a list of `None`, which is a loop wearing a disguise and wastes memory proportional to the input. Second, comprehensions are expressions, so they cannot contain `try/except`, `break`, `continue` or an assignment statement; per-item error handling or early exit forces a loop. Third, readability: once there are several clauses, a long output expression or a condition you have to re-read, extract a named helper function or expand it. A useful bar is that a comprehension should be sayable in one breath.

code

python · 6 lines
python
items = ["a", "b"]

_ = [print(x) for x in items]

for x in items:
    print(x)

go deeper

for a junior

Know that a comprehension is for producing a container. If you are not using the value it returns, write a for loop instead — that alone avoids the most common misuse.

for a middle

Explain the hard limits: comprehensions are expressions, so no try/except, no break, no continue, no assignment statement inside. Be ready to convert a dense one into a named helper plus a short comprehension.

for a senior

Bring judgment rather than a rule: match the construct to the intent, notice double passes over a source, and catch the side-effect comprehension in review with the memory argument, not just a style preference.

for a principal

Own the house convention and keep it cheap to follow — where the line sits, which checks enforce it automatically, and how you stop review time being spent relitigating comprehension length on every pull request.

A comprehension is a good tool with a narrow job: *produce a container by mapping and optionally filtering another iterable.* Almost every bad comprehension is one that was asked to do something else. ## 1. When you want the side effect, not the container ```python [send_alert(x) for x in flagged] # wrong ``` This builds a list of `None` values the same length as `flagged`, and immediately throws it away. On a large input that is real memory for no result, and to a reader it is actively misleading: the brackets promise a value that is never used. The `for` loop says exactly what is happening: ```python for x in flagged: send_alert(x) ``` The same objection applies to a comprehension assigned to `_`. If the return value is not wanted, the comprehension is not the construct. ## 2. When you need a statement Comprehensions are **expressions**, and expressions cannot contain statements. That rules out, per item: - `try` / `except` — you cannot skip a row whose value fails to parse; - `break` — you cannot stop early once you have found what you need; - `continue` — the trailing `if` filter covers the simple case, but nothing more; - an assignment statement, or anything that mutates local state as it goes. Per-item error handling is the most common of these in real code. A batch of rows where some values are malformed simply cannot be expressed as one comprehension without either pre-validating in a separate pass or hiding the failure. Write the loop: ```python parsed = {} for row in rows: try: parsed[row["id"]] = float(row["score"]) except ValueError: continue ``` Early exit is the second: searching for the first item that satisfies a condition with a comprehension processes the entire input, whereas a loop with `break` stops at the hit. ## 3. When it has outgrown one breath There is no rule in the language about length, so teams invent one. A workable bar: a comprehension should be readable aloud as a single sentence. When any of these are true, it has passed the point: - the output expression is itself a multi-part expression, especially a conditional expression with a non-trivial condition; - the filter needs a comment to explain it; - the whole thing no longer fits on one line and has to be broken across several. The fix is usually not "write a loop" but "name something". Pull the output expression into a small function with a name that says what it computes, and the comprehension collapses back to `[normalised(row) for row in rows if is_valid(row)]` — which reads better than either the dense comprehension or the expanded loop. Nesting a comprehension inside another expression, for instance inside an f-string or as one argument among several in a long call, has the same effect; give it a name on its own line first. ## 4. When you build more than one thing Two comprehensions over the same source make two passes: ```python valid = [r for r in rows if ok(r)] rejected = [r for r in rows if not ok(r)] ``` Besides calling `ok` twice per row, this states the partition twice, so the two can drift apart when one is edited. A single loop appending to two lists is both faster and honest about the fact that one decision produces both outputs. ## 5. When materialising is the wrong shape A list comprehension is eager: it builds and holds every element. If the result is only ever iterated once and is very large, holding it is a cost with no benefit — the parenthesised lazy form exists for exactly that, and choosing between them is its own topic. The relevant judgement here is simply that *the container is not free*, and a comprehension always pays for one. ## What to say in an interview Frame it as intent rather than as a rule list. A comprehension is a declaration of what the result is; a loop is a description of what happens step by step. When the interesting thing about the code is the result, comprehension; when the interesting thing is the process — retries, error handling, early exit, logging, accumulating into several places — the loop is not a fallback, it is the correct construct. Reviewers who apply that test rarely argue about comprehension length, because the badly-sized ones are almost always the ones doing a process job in a result-shaped syntax.

  • How would you keep a comprehension when the output expression has become hard to read?
    Extract it. Move the computation into a small named function and call that from the comprehension, so the line becomes `[normalised(row) for row in rows if is_valid(row)]`. The comprehension keeps stating the shape of the result, the name carries the detail, and the function is independently testable — usually better than expanding to a loop.
  • What is wrong with writing two comprehensions over the same source to split it in two?
    It walks the source twice and evaluates the predicate twice per item, and it states the partition rule in two places that can drift when one is edited. A single loop that appends to both lists evaluates the decision once and keeps the two branches literally adjacent.
  • Is a comprehension used purely for its side effects merely a style issue?
    No. It allocates a list the size of the input to hold values nobody reads, which on a large batch is genuine memory pressure, and it misleads every later reader into looking for the result. Style and cost point the same way here.

A comprehension is a recipe card listing what the dish is made of; a loop is the cook's running commentary. When the interesting part is what went wrong on step three, the card is the wrong format.

saying these in an interview costs you the question

  • Uses a comprehension purely to run a function per item
  • Claims any loop can be rewritten as a comprehension
  • Thinks try/except can appear inside a comprehension
  • Treats comprehension length as the only readability rule
  • Believes comprehensions are always faster so always better
  • Splits a source with two comprehensions instead of one loop

context