This PEP proposes extending list, set, and dictionary comprehensions, as well as generator expressions, to allow unpacking notation (* and **) at the start of the expression, providing a concise way of combining an arbitrary number of iterables into one list or set or generator, or an arbitrary number of dictionaries into one dictionary, for example:
Extended unpacking notation (* and **) from PEP 448 makes it easy to combine a few iterables or dictionaries:
But if we want to similarly combine an arbitrary number of iterables, we cannot use unpacking in this same way.
That said, we do have a few options for combining multiple iterables. We could, for example, use explicit looping structures and built-in means of combination:
Or, we could be more concise by using a comprehension with two loops:
Or, we could use itertools.chain or itertools.chain.from_iterable:
Or, for all but the generator, we could use functools.reduce:
This PEP proposes allowing unpacking operations to be used in comprehensions as an additional alternative:
This proposal also extends to asynchronous comprehensions and generator expressions, such that, for example, (*ait async for ait in aits()) is equivalent to (x async for ait in aits() for x in ait).
Combining multiple iterable objects together into a single object is a common task. For example, one StackOverflow post asking about flattening a list of lists has been viewed 4.6 million times, and there are several examples of code from the standard library that perform this operation (see Code Examples). While Python provides a means of combining a small, known number of iterables using extended unpacking from PEP 448, no comparable syntax currently exists for combining an arbitrary number of iterables.
This proposal represents a natural extension of the language, paralleling existing syntactic structures: where [x, y, z] creates a list from a fixed number of values, [item for item in items] creates a list from an arbitrary number of values; this proposal extends that notion to the construction of lists that involve unpacking, making [*item for item in items] analogous to [*x, *y, *z].
We expect this syntax to be intuitive and familiar to programmers already comfortable with both comprehensions and unpacking notation. This proposal was motivated in part by a written exam in a Python programming class, where several students used the proposed notation (specifically the set version) in their solutions, assuming that it already existed in Python. This suggests that the notation represents a logical, consistent extension to Python’s existing syntax. By contrast, the existing double-loop version [x for it in its for x in it] is one that students often get wrong, the natural impulse for many students being to reverse the order of the for clauses. The intuitiveness of the proposed syntax is further supported by the comment section of a Reddit post made following the initial publication of this PEP, which demonstrates support from a broader community.
The grammar should be changed to allow the expression in list/set comprehensions and generator expressions to be preceded by a *, and allowing an alternative form of dictionary comprehension in which a double-starred expression can be used in place of a key: value pair.
This can be accomplished by updating the listcomp and setcomp rules to use star_named_expression instead of named_expression:
The rule for genexp would similarly need to be modified to allow a starred_expression:
The rule for dictionary comprehensions would need to be adjusted as well, to allow for this new form:
No change should be made to the way that argument unpacking is handled in function calls, i.e., the general rule that generator expressions provided as the sole argument to functions do not require additional redundant parentheses should be retained. Note that this implies that, for example, f(*x for x in it) is equivalent to f((*x for x in it)) (see Starred Generators as Function Arguments for more discussion).
* and ** should only be allowed at the top-most level of the expression in the comprehension (see Further Generalizing Unpacking Operators for more discussion).
The meaning of a starred expression in a list comprehension [*expr for x in it] is to treat each expression as an iterable, and concatenate them, in the same way as if they were explicitly listed via [*expr1, *expr2, ...]. Similarly, {*expr for x in it} forms a set union, as if the expressions were explicitly listed via {*expr1, *expr2, ...}; and {**expr for x in it} combines dictionaries, as if the expressions were explicitly listed via {**expr1, **expr2, ...}. These operations should retain all of the equivalent semantics for combining collections in this way (including, for example, later values replacing earlier ones in the case of a duplicated key when combining dictionaries).
Said another way, the objects created by the following comprehensions:
should be equivalent to the objects created by the following pieces of code, respectively:
Generator expressions using the unpacking syntax should form new generators producing values from the concatenation of the iterables given by the expressions. Specifically, the behavior is defined to be equivalent to the following (though without defining or referencing the looping variable i):
See Alternative Generator Expression Semantics for more discussion of these semantics.
Note that this proposal does not suggest changing the order of evaluation of the various pieces of the comprehension, nor any rules about scoping. This is particularly relevant for generator expressions that make use of the “walrus operator” := from PEP 572, which, when used in a comprehension or a generator expression, performs its variable binding in the containing scope rather than locally to the comprehension.
As an example, consider the generator that results from evaluating the expression (*(y := [i, i+1]) for i in (0, 2, 4)). This is approximately equivalent to the following generator, except that in its generator expression form, y will be bound in the containing scope instead of locally:
In this example, the subexpression (y := [i, i+1]) is evaluated exactly three times before the generator is exhausted: just after assigning i in the comprehension to 0, 2, and 4, respectively. Thus, y (in the containing scope) will be modified at those points in time:
Currently, the proposed syntax generates a SyntaxError. Allowing these forms to be recognized as syntactically valid requires adjusting the grammar rules for invalid_comprehension and invalid_dict_comprehension to allow the use of * and **, respectively.
Additional specific error messages should be provided in at least the following cases:
The reference implementation implements this functionality, including draft documentation and additional test cases.
The behavior of all comprehensions that are currently syntactically valid would be unaffected by this change, so we do not anticipate much in the way of backwards-incompatibility concerns. In principle, this change would only affect code that relied on the fact that attempting to use unpacking operations in comprehensions would raise a SyntaxError, or that relied on the particular phrasing of any of the old error messages being replaced, which we expect to be rare.
One related concern is that a hypothetical future decision to change the semantics of generator expressions to make use of yield from during unpacking (delegating to generators that are being unpacked) would not be backwards-compatible because it would affect the behavior of the resulting generators when used with .send()/.asend(), .throw()/.athrow(), and .close()/.aclose(). That said, despite being backwards-incompatible, such a change would be unlikely to have a large impact because it would only affect the behavior of structures that, under this proposal, are not particularly useful. See Alternative Generator Expression Semantics for more discussion.
This section shows some illustrative examples of how small pieces of code from the standard library could be rewritten to make use of this new syntax. The Reference Implementation continues to pass all tests with these replacements made.
Replacing explicit loops compresses multiple lines into one, and avoids the need for defining and referencing an auxiliary variable.
While not always the right choice, replacing itertools.chain.from_iterable and map can avoid an extra level of redirection, resulting in code that follows conventional wisdom that comprehensions are generally more readable than map/filter.
Replacing double loops in comprehensions avoids the need for defining and referencing an auxiliary variable.
Currently, a common way to introduce the notion of comprehensions (which is employed by the Python Tutorial) is to demonstrate equivalent code. For example, this method would say that, for example, out = [expr for x in it] is equivalent to the following code:
Taking this approach, we can introduce out = [*expr for x in it] as instead being equivalent to the following (which uses extend instead of append):
Set and dict comprehensions that make use of unpacking can also be introduced by a similar analogy:
And we can take a similar approach to illustrate the behavior of generator expressions that involve unpacking:
We can then generalize from these specific examples to the idea that, wherever a non-starred comprehension/genexp would use an operator that adds a single element to a collection, the starred would instead use an operator that adds multiple elements to that collection.
Alternatively, we don’t need to think of the two ideas as separate; instead, with the new syntax, we can think of out = [...x... for x in it] as equivalent to the following [1] (where ...x... is a stand-in for arbitrary code), regardless of whether or not ...x... uses *:
Similarly, we can think of out = {...x... for x in it} as equivalent to the following code, regardless of whether or not ...x... uses * or ** or ::
These examples are equivalent in the sense that the output they produce would be the same in both the version with the comprehension and the version without it, but note that the non-comprehension version is slightly less efficient due to making new lists/sets/dictionaries before each extend or update, which is unnecessary in the version that uses comprehensions.
The primary goal when thinking through the specification above was consistency with existing norms around unpacking and comprehensions / generator expressions. One way to interpret this is that the goal was to write the specification so as to require the smallest possible change(s) to the existing grammar and code generation, letting the existing code inform the surrounding semantics.
Below we discuss some of the common concerns/alternative proposals that came up in discussions but that are not included in this proposal.
One common concern that has arisen multiple times (not only in the discussion threads linked above but also in previous discussions around this same idea) is a possible syntactical ambiguity when passing a starred generator as the sole argument to f(*x for x in y). In the original PEP 448, this ambiguity was cited as a reason for not including a similar generalization as part of the proposal.
This proposal suggests that f(*x for x in y) should be interpreted as f((*x for x in y)) and should not attempt further unpacking of the resulting generator, but several alternatives were suggested in our discussion (and/or have been suggested in the past), including:
The reason to prefer this proposal over these alternatives is the preservation of existing conventions for punctuation around generator expressions. Currently, the general rule is that generator expressions must be wrapped in parentheses except when provided as the sole argument to a function, and this proposal suggests maintaining that rule even as we allow more kinds of generator expressions. This option maintains a full symmetry between comprehensions and generator expressions that use unpacking and those that don’t.
Currently, we have the following conventions:
This proposal opts to maintain those conventions even when the comprehensions make use of unpacking:
Another suggestion that came out of the discussion involved further generalizing the * beyond simply allowing it to be used to unpack the expression in a comprehension. Two main flavors of this extension were considered:
These variants were considered substantially more complex (both to understand and to implement) and of only marginal utility, so neither is included in this PEP. As such, these forms should continue to raise a SyntaxError, but with a new error message as described above, though it should not be ruled out as a consideration for future proposals.
Another point of discussion centered around the semantics of unpacking in generator expressions, particularly the relationship between the semantics of synchronous and asynchronous generator expressions given that async generators do not support yield from (see the section of PEP 525 on Asynchronous yield from).
The core question centered around whether sync and async generator expressions should use yield from (or an equivalent) when unpacking, as opposed to an explicit loop. The main difference between these options is whether the resulting generator delegates to the objects being unpacked, which would affect the behavior of these generator expressions when used with .send()/.asend(), .throw()/.athrow(), and .close()/.aclose() in the case where the objects being unpacked are themselves generators. The differences between these options are summarized in Appendix: Semantics of Generator Delegation.
Several reasonable options were considered, none of which was a clear winner in a poll in the Discourse thread. Beyond the proposal outlined above, the following were also considered:
This strategy would have allowed unpacking in generator expressions to closely mimic a popular way of writing generators that perform this operation (using yield from), but it would also have created an asymmetry between synchronous and asynchronous versions, and also between this new syntax and itertools.chain and the double-loop version.
This strategy would also make unpacking in synchronous and asynchronous generators behave symmetrically, but it would also be more complex, enough so that the cost may not be worth the benefit, particularly in the absence of a compelling use case for delegation.
This strategy could possibly reduce friction if asynchronous generator expressions do gain support for yield from in the future by making sure that any decision made at that point would be fully backwards-compatible, but in the meantime, it would result in an even bigger discrepancy between synchronous and asynchronous generator expressions than option 1.
This would retain symmetry between the two cases, but with the downside of losing an expressive form and reducing symmetry between list/set comprehensions and generator expressions.
Each of these options (including the one presented in this PEP) has its benefits and drawbacks, with no option being clearly superior on all fronts. The semantics proposed in Semantics: Generator Expressions above represent a reasonable compromise by allowing exactly the same kind of unpacking in synchronous and asynchronous generator expressions and retaining an existing property of generator expressions (that they do not delegate to subgenerators).
This decision should be revisited in the event that asynchronous generators receive support for yield from in the future, in which case adjusting the semantics of unpacking in generator expressions to use yield from should be considered.
Although the general consensus from the discussion thread seemed to be that this syntax was clear and intuitive, several concerns and potential downsides were raised as well. This section aims to summarize those concerns.
Quite a few other languages support this kind of flattening with syntax similar to what is already available in Python, but support for using unpacking syntax within comprehensions is rare. This section provides a brief summary of support for similar syntax in a few other languages.
Many languages that support comprehensions support double loops:
Several other languages (even those without comprehensions) support these operations via a built-in function or method to support flattening of nested structures:
However, languages that support both comprehension and unpacking do not tend to allow unpacking within a comprehension. For example, the following expression in Julia currently leads to a syntax error:
As one counterexample, support for a similar syntax was recently added to Civet. For example, the following is a valid comprehension in Civet, making use of JavaScript’s ... syntax for unpacking:
One of the common questions about the semantics outlined above had to do with the difference between using yield from when unpacking inside of a generator expression, versus using an explicit loop. Because this is a fairly-advanced feature of generators, this appendix attempts to summarize some of the key differences between generators that use yield from and those that use explicit loops.
For simple iteration over values, which we expect to be by far the most-common use of unpacking in generator expressions, both approaches produce identical results:
The differences become apparent when using the advanced generator protocol methods .send(), .throw(), and .close(), and when the sub-iterables are themselves generators rather than simple sequences. In these cases, the yield from version results in the associated signal reaching the subgenerator, but the version with the explicit loop does not.
This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.
Source: https://github.com/python/peps/blob/main/peps/pep-0798.rst
Last modified: 2026-06-03 14:19:42 UTC