Unpacking for Python comprehensions
Unpacking Python iterables of various sorts, such as dictionaries or lists, is useful in a number of contexts, including for function arguments, but there has long been a call for extending that capability to comprehensions. PEP 798 ("Unpacking in Comprehensions") was first proposed in June 2025 to fill that gap. In early November, the steering council accepted the PEP, which means that the feature will be coming to Python 3.15 in October 2026. It may be something of a niche feature, but it is an inconsistency that has been apparent for a while—to the point that some Python programmers assume that it is already present in the language.
Unpacking
One of the most common use cases for unpacking is to pass a list or dictionary to a function as a series of its elements rather than as a single object. The "*" unpacking operator (and its companion, "**", for dictionaries) can be used to expand an iterable in that fashion; a simple example might look like the following:
def foo(a, b):
a*b
l = [ 2, 4 ] # list
t = ( 3, 5 ) # tuple
d = { 'a' : 2, 'b' : 9 } # dict
foo(*l) # foo(2, 4) == 8
foo(*t) # foo(3, 5) == 15
foo(**d) # foo(a=2, b=9) == 18
In each case, the unpacking operator extracts the elements of the iterable
to turn them into individual arguments to the function—keyword
arguments for the dictionary.
Python comprehensions provide a mechanism to build a list or other iterable using a compact syntax, rather than a full loop. A classic example of that appears in the Python documentation linked just above:
squares = []
for x in range(10):
squares.append(x**2)
# can be replaced with
squares = [ x**2 for x in range(10) ]
For both of those, the result is a list with the squares of the numbers
zero through nine.
The unpacking operator can already be used to create iterables, such as:
a = [ 1, 2 ]
b = [ 3, 4 ]
c = [ *a, *b ] # [ 1, 2, 3, 4 ]
# dictionaries can be merged in similar fashion
newdict = { **d1, **d2, **d3 }
In the latter example, the order of the dictionaries matters, keys that are
duplicated will take their value from the last dictionary where they were set
(i.e. d3[key] takes precedence over the value for key in d1 or d2).
But what if there is a list of lists with a length that is not known except at run time? Currently, there is no easy way to use a list comprehension to build the flattened list of all of the entries of each list; it can be done using a comprehension with two loops, but that is error prone. There are other possibilities too, as described in the "Motivation" section of the PEP, but all of them suffer from semi-obscurity or complexity. Instead, Python will be allowing unpacking operators in comprehensions:
# from the PEP
[*it for it in its] # list with the concatenation of iterables in 'its'
{*it for it in its} # set with the union of iterables in 'its'
{**d for d in dicts} # dict with the combination of dicts in 'dicts'
(*it for it in its) # generator of the concatenation of iterables in 'its'
# a usage example
a = [ 1, 2 ]
b = [ 3, 4, 5 ]
c = [ 6 ]
its = [ a, b, c ]
[ *it for it in its ] # [ 1, 2, 3, 4, 5, 6 ]
# current double-loop version
[ x for it in its for x in it ] # [ 1, 2, 3, 4, 5, 6 ]
Discussion
The PEP authors, Adam Hartz and Erik Demaine, actually proposed
the idea back in 2021 on the python-ideas mailing list. As noted in
that message, though, the idea also came
up in 2016 and perhaps even before that. In 2021, the proposal was
generally well-received, and reached the
pre-PEP
stage, but was unable to attract a core developer as a sponsor. In
late June, Hartz posted
a lengthy pre-PEP message to the ideas category of the Python discussion forum, "hoping to find a sponsor for moving forward with the PEP process if there's still enthusiasm behind this idea
".
Hartz noted that the idea had been raised in October 2023, as well, so it is a feature that is frequently brought up—generally to nodding approval. That 2023 message was posted by Alex Prengère, who was quick to reply to the pre-PEP saying that he had been working on unpacking in comprehensions as well. He, along with others, wondered about support for unpacking in asynchronous comprehensions (as described in PEP 530); it was not mentioned in the pre-PEP, but the implementation would allow them, he said. Hartz said that the intent was to support them and that he would update the text to reflect that.
There was also some discussion regarding the syntax for making function calls using a generator comprehension (e.g. f(x for x in it)); there is some ambiguity in the meaning, as Ben Hsing noted. PEP 448 ("Additional Unpacking Generalizations") added the ability to use the unpacking operators in more contexts, including function call arguments, but it explicitly did not extend that to generator comprehensions because it was not clear which meaning should be chosen. As Hsing put it:
That is, which one of these is intended?f(*x for x in it) == f((*x for x in it))or:f(*x for x in it) == f(*(x for x in it))
In the thread, Hartz and others argued that, since the language already allows a generator comprehension (without any unpacking operators) as an argument without requiring an extra set of parentheses (e.g. f(x for x in it), the same should be true for those with the unpacking operator. In the reply linked above, Hartz noted that the error message for the syntax error points in that direction as well:
A little bit of support in this direction, perhaps, comes from the way that the syntax error for f(*x for x in it) is reported in 3.13, which suggests that this is interpreted as f(<a single malformed generator expression>) rather than as f(*<something>):
>>> f(*x for x in its)
File "<python-input-0>", line 1
f(*x for x in its)
^^
SyntaxError: iterable unpacking cannot be used in comprehension
The draft PEP continued to be discussed; it was updated
a few days after it was first posted, on June 25, and then again
on July 3. The latter posting caught the eye of core developer Jelle
Zijlstra who said
that it "is a nice little feature that I've missed several times in the
past
" and that he was willing to sponsor it. That started its path
into the CPython mainline.
The now-numbered PEP 798 was posted for discussion in the PEPs category of the forum on July 19. Along the way, the PEP had picked up some extra pieces, including a section with examples of where standard library code could be simplified using the feature and an appendix on support in other languages. Most of the comments at that point were about other features that might also be considered, though Hartz and Zijlstra tried to keep things focused on the PEP itself.
One outstanding issue was the treatment of synchronous generator expressions versus the asynchronous variety. The PEP, which will be changing as we will see, currently makes a distinction between the two because "yield from" is not permitted in asynchronous generators. Another appendix goes into more detail; the difference comes down to whether the generator-protocol methods, such as send(), can be used. There are two ways that the semantics of an unpacking generator expression could be defined:
g = (*x for x in it)
# could be:
def gen():
for x in it:
yield from x
g = gen()
# or:
def gen():
for x in it:
for i in x:
yield i
g = gen()
Either of those works for synchronous generators, but:
g = (*x async for x in ait())
# must be:
async def gen()
async for x in ait():
for i in x:
yield i
g = gen()
So the question is whether it makes sense to define the synchronous semantics differently
so that those comprehensions could potentially use the generator-protocol methods.
Hartz ran a poll
in the thread, with several possibilities for the semantics, but no real
consensus was reached—perhaps unsurprising given the esoteric nature of the
question and that thread participants had likewise been unable to converge
on the semantics.
In mid-September, after more than a month of quiet in the thread, Hartz submitted
the PEP to the steering council for consideration. The council started
looking at it a month later, with council member Pablo Galindo Salgado noting
that the group was uncomfortable with positioning the new syntax "as
offering 'much clearer' code compared to existing alternatives (such as
itertools.chain.from_iterable, explicit loops, or nested
comprehensions)
" because readability is in the eye of the beholder.
Instead, the council suggested that "the stronger, more objective argument is
syntactic consistency as extending Python's existing unpacking
patterns naturally into comprehensions
". Hartz agreed
and adjusted the PEP accordingly.
In the thread, "Nice Zombies" highlighted part of the "Rationale" section of the PEP, which nicely illustrates the argument for syntactic consistency:
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.
One of the examples given in the PEP shows how an explicit loop to create a set could be changed in the shutil module of the standard library:
# current:
ignored_names = []
for pattern in patterns:
ignored_names.extend(fnmatch.filter(names, pattern))
return set(ignored_names)
# proposed:
return {*fnmatch.filter(names, pattern) for pattern in patterns}
Instead of extending the list from the iterable
returned by fnmatch.filter(), then converting it to a set, the new
syntax allows creating the set directly. The existing code could have taken
advantage of set.update()
to avoid using the list, but the new syntax is in keeping with the ideas
behind comprehensions—and was apparently intuitively obvious, but wrong, to
Python students.
In its announcement of the PEP's acceptance, the SC also decided the
question about generator comprehensions: "we require that both
synchronous and asynchronous generator expressions use explicit loops
rather than yield from for unpacking operations
". That
removes some advanced use cases "that are rarely relevant when writing
comprehensions
" but simplifies the mental model for the new feature. "We don't believe that developers writing comprehensions should have to think about the differences between sync and async generator semantics or about generator delegation protocols.
"
While it is certainly useful, the feature is not revolutionary in any sense, it simply fills a fairly longstanding hole that has been noticed and discussed several times over the years. Python is a mature language at this point, so revolutions are likely to be few and far between—if not absent entirely. The whole tale shows, however, that, with some persistence, a well-written PEP, and a well-shepherded discussion (by Hartz joined by Zijlstra, Demaine was absent this time around), changes can be made. Future Python students can rejoice starting next October.
| Index entries for this article | |
|---|---|
| Python | Enhancements |
| Python | Python Enhancement Proposals (PEP)/PEP 798 |
