Flattening a list of lists has never had an obvious spelling. There are three common ones and everybody re-derives the loop ordering of the second from scratch every time:
list(itertools.chain.from_iterable(groups))
[item for group in groups for item in group]
functools.reduce(operator.iadd, groups, [])
Python 3.15 adds a fourth that says what it means:
[*group for group in groups]
The forms
[*L for L in lists] # list comprehension
{*s for s in sets} # set comprehension
{**d for d in dicts} # dict comprehension; later keys win
(*L for L in lists) # generator expression
[*chunk async for chunk in stream] # async comprehensions too
The semantics are exactly the semantics of unpacking in a display: each element
expression contributes zero or more items, in order. For {**d for d in dicts}
the last occurrence of a key wins, which matches {**a, **b} and dict.update.
💡[*L for L in lists] and [item for L in lists for item in L] produce the same list. Give two reasons to prefer the first that are not "it is shorter".
click to reveal
Loop-order clarity. In the double-for form, the two clauses read left to right as outer-then-inner, which is the opposite of the order they appear in the equivalent nested for statements for many people’s intuition — hence the perennial “which one goes first” hesitation. There is no ordering to get wrong in [*L for L in lists] because there is only one loop.
It generalises to a computed expression. [*transform(L) for L in lists] needs no temporary name; the double-for version needs [item for L in lists for item in transform(L)], which introduces item purely as plumbing. Every name you do not introduce is a name that cannot collide or be misread.
A third, more subtle one: the unpacking form makes the shape of the element explicit at the point of the element expression. When you are reading a comprehension whose body is a long call, * at the front tells you immediately that the call returns an iterable that is being spliced, not appended.
The restrictions, all five of them
-
Top level of the element expression only.
[*L for L in lists]is fine;[(*L,) for L in lists]is a different thing entirely (a tuple per group), and[f(*L) for L in lists]is an ordinary call, not a splice. -
`
is aSyntaxErrorin list, set and generator forms.** Only the dict comprehension takes**`. -
Not inside a dict comprehension’s key or value.
{k: *v for ...}is not a thing — the dict form’s**dreplaces the wholekey: valueslot. -
A conditional expression needs parentheses.
[*(a if cond else b) for ...]— without them the parser cannot tell where the unpacked expression ends. -
f(*x for x in y)passes the generator as ONE argument. It does not splat the flattened items into the call. If you want that, be explicit:f(*[*x for x in y]).
Restriction 5 is the one that will catch people. f(*gen) and
f(*x for x in y) look similar and mean completely different things; the second
is f(<generator>) with a * inside the generator’s element expression.
💡Why is {**d for d in dicts} genuinely more useful than {**a, **b, **c}, given that both merge dicts with "later wins"?
click to reveal
Because the display form requires you to know the number of dicts at the point you write the code, and the comprehension does not.
{**a, **b, **c} is a fixed arity: three literals in the source. The moment the layers come from a list — a config loaded from N files, a chain of defaults per environment, a merge across an arbitrary number of plugin contributions — the display form cannot express it and you fall back to a loop with result.update(layer), or functools.reduce(operator.or_, layers, {}), or dict(ChainMap(*reversed(layers))) with its own ordering gotcha.
{**layer for layer in layers} is the same “later wins” semantics at arbitrary arity, in one expression, with no accumulator to initialise and no reversal to get wrong.
One caveat worth stating: like {**a, **b}, the comprehension produces a plain dict. If the layers are defaultdicts or Counters, that behaviour does not survive — see the modernisation table’s note on | versus {**a, **b}.
Why this track has no problem for it
Every other item in T4 ships with an exercise. This one does not, and the reason is worth stating plainly rather than hiding.
[*L for L in lists] is syntax, available from 3.15. The grader executes
submissions on the learner’s own machine through the local runtime, and the
runtime’s interpreter is 3.14. A submission using the new form does not fail a
test — it fails to parse, at import, before the harness can call anything.
There is no runtime feature check that can degrade gracefully, because the
failure happens during compilation of the module.
We could have written an exercise that asks for chain.from_iterable and
merely mentions PEP 798 in the prose. That would be a problem about
itertools wearing this article’s title, and it would teach the learner that
the exercise’s answer is the thing to write — which is precisely wrong here.
Better to ship the article, and add the exercise when 3.15 is the floor.
Until then, on 3.12–3.14, the correct spellings are:
list(chain.from_iterable(groups)) # flatten
set().union(*sets) # union of sets
functools.reduce(operator.or_, layers, {}) # merge dicts, later wins
and requires-python = ">=3.15" is what you will add on the day you switch.