The nested comprehension and the loop it is shorthand for. Read them side by side until the order stops surprising you.
rows = [[1, 2], [3, 4]]
result = []
for row in rows: # outer loop first
for item in row: # inner loop second
result.append(item)
same = [item for row in rows for item in row]
# ^^^^ what you keep ^^^^ outer ^^^^ inner
print(result == same) # -> True
- The for clauses appear in the same order as nested loops
Left to right in the comprehension is outer to inner in the loop. The confusion comes from the expression being at the *front* while its loops trail behind it — everything after the first for reads top-down exactly as you would write it out.
- The inner clause can use the outer name
`for item in row` works because row was already bound by the clause to its left. Swap them and you get a NameError, which is the quickest way to confirm you have them the wrong way round.
- One level, because you only wrote one inner loop
Nothing here recurses, so a list inside a row is simply an item and arrives intact. That is a decision rather than a limitation — flattening all the way down is a different function with different edge cases.
Two loops: one over the rows, one over the items inside each row. Write it as a plain nested loop first, then read the worked example again.
In a comprehension the for clauses go in the same order as the nested loops — outer first. The expression you keep goes at the front, before either of them.
[item for row in rows for item in row]. Nothing is appended, nothing is extended, and the input is never touched — a comprehension always builds something new.
def flatten(rows): return [____ for ____ in rows for ____ in ____]
def flatten(rows):
return [item for row in rows for item in row]
result = []; for row in rows: result.extend(row) is exactly equivalent and three lines longer. It is also the version that tempts you into `rows[0].extend(...)` to save an allocation, which quietly grows the caller’s first row. The comprehension cannot make that mistake because it has nowhere to put it.
Two of the cases exist to pin it down: a nested list stays nested, and a string stays a string. A 'flatten anything' version needs to decide what counts as flattenable, and the honest answer involves isinstance checks and a special case for strings — which is why the general one is a different, harder function.
itertools.chain.from_iterable(rows) is the same operation, lazily — it yields items instead of building a list, so a million rows never have to be in memory twice. Reach for it when the result feeds straight into another loop; the comprehension is clearer when you want a list anyway.
If the rows are coming from a generator and you are about to iterate the result once, do not build a list at all — chain.from_iterable keeps it lazy. If the data is numeric and large, a NumPy reshape or ravel will beat any Python loop by an order of magnitude. And if you find yourself flattening immediately after grouping, check whether the grouping needed to happen at all.
- Write flatten_all that goes all the way down, and decide what it should do with a string — then notice that decision is the hard part, not the recursion.
- Make it lazy with yield, and check what changes for a caller who wanted len().
- Accept any iterable of iterables — a generator of tuples, a dict of lists — and see how little has to change.