Python unpacking in comprehensions
last modified October 29, 2026 Python 3.15
In this article we show how to use the * and **
unpacking operators inside comprehensions and generator expressions. The
feature was introduced in Python 3.15 by PEP 798.
Unpacking has long been allowed in list, set, and dictionary displays:
[*a, *b] joins two lists and {**x, **y} merges two
dictionaries. Those displays work only when the number of pieces is known
when the code is written. When the pieces come from a loop, earlier Python
versions required a nested comprehension, itertools.chain, or an
explicit loop.
Python 3.15 lets the expression part of a comprehension be a starred expression. Each item produced by the loop is unpacked into the result instead of being added to it as a single element.
[*expr for x in iterable] # list
{*expr for x in iterable} # set
{**expr for x in iterable} # dict
(*expr for x in iterable) # generator
The *expr must evaluate to an iterable; **expr must
evaluate to a mapping. All the usual parts of a comprehension, such as several
for clauses and if conditions, still work.
Flattening a list of lists
The most common use of the new syntax is flattening a list of lists into a single list.
matrix = [[1, 2, 3], [4, 5], [6, 7, 8, 9]] flat = [*row for row in matrix] print(flat) nested = [e for row in matrix for e in row] print(nested) print(flat == nested)
The example flattens a matrix whose rows have different lengths, first with unpacking and then with the classic nested comprehension.
flat = [*row for row in matrix]
For each row, the *row expression unpacks its
elements into the new list. It reads as "all the elements of each row".
nested = [e for row in matrix for e in row]
The equivalent comprehension with two for clauses needs an extra
variable, and its clause order is a common source of mistakes. Both forms
produce the same list.
$ python flatten.py [1, 2, 3, 4, 5, 6, 7, 8, 9] [1, 2, 3, 4, 5, 6, 7, 8, 9] True
Set comprehensions
A starred expression in a set comprehension adds every element of each iterable to the set. Duplicates are removed as usual.
posts = [
{'title': 'Lazy imports', 'tags': ['python', 'imports', 'performance']},
{'title': 'frozendict', 'tags': ['python', 'types']},
{'title': 'Sampling profiler', 'tags': ['python', 'performance', 'tools']},
]
tags = {*post['tags'] for post in posts}
print(sorted(tags))
We have a list of blog posts, each with a list of tags. We want all the distinct tags used across the posts.
tags = {*post['tags'] for post in posts}
The tags of each post are unpacked into the set. The python and
performance tags occur several times but are stored only once. We
sort the set to get a stable output.
$ python unique_tags.py ['imports', 'performance', 'python', 'tools', 'types']
Merging dictionaries
Double-star unpacking in a dictionary comprehension merges a variable number of mappings into one dictionary.
defaults = {'host': 'localhost', 'port': 8000, 'debug': False}
config_file = {'port': 8080, 'workers': 4}
env = {'debug': True}
layers = [defaults, config_file, env]
settings = {**layer for layer in layers}
print(settings)
The example builds application settings from layered sources: defaults, a configuration file, and environment overrides.
settings = {**layer for layer in layers}
The mappings are merged in iteration order. When a key occurs in more than one
layer, the value from the later layer wins, just as with
{**defaults, **config_file, **env}. Unlike the display form, the
comprehension works for any number of layers.
$ python merge_dicts.py
{'host': 'localhost', 'port': 8080, 'debug': True, 'workers': 4}
Unpacking with a condition
Conditions filter the items before they are unpacked.
text = """\
# fruits
apple banana
cherry
# vegetables
carrot pea bean
"""
words = [*line.split() for line in text.splitlines()
if line and not line.startswith('#')]
print(words)
We collect all words from a text, skipping empty lines and comment lines.
words = [*line.split() for line in text.splitlines()
if line and not line.startswith('#')]
The if clause decides which lines are used. The words of each
remaining line are unpacked into the result.
$ python filter_unpack.py ['apple', 'banana', 'cherry', 'carrot', 'pea', 'bean']
Conditional expressions
A conditional expression can choose what to unpack, but it must be put in parentheses.
groups = [[1, 2], None, [3], None, [4, 5]] values = [*(g if g is not None else [0]) for g in groups] print(values) values = [*g for g in groups if g is not None] print(values)
The groups list contains lists and None values.
values = [*(g if g is not None else [0]) for g in groups]
The conditional expression picks either the group or a placeholder list, and
the star unpacks the chosen value. Without the parentheses, Python reports a
syntax error, because it is not clear whether the star belongs to the whole
expression or only to g.
values = [*g for g in groups if g is not None]
When the missing values should be skipped rather than replaced, an
if clause is simpler.
$ python conditional.py [1, 2, 0, 3, 0, 4, 5] [1, 2, 3, 4, 5]
Unpacking any iterable
The starred expression can be any iterable, not just a list.
words = ['sky', 'blue']
print([*word for word in words])
print([*range(n) for n in range(1, 4)])
stock = [{'apples': 4, 'pears': 2}, {'plums': 7}]
print([*shelf for shelf in stock])
print([*shelf.items() for shelf in stock])
The example unpacks strings, ranges, and dictionaries.
print([*word for word in words])
Unpacking a string produces its characters.
print([*range(n) for n in range(1, 4)])
Each range object is expanded into its numbers.
print([*shelf for shelf in stock]) print([*shelf.items() for shelf in stock])
A single star on a dictionary unpacks its keys, the same as iterating over the
dictionary. To get the key-value pairs, we unpack the items view.
$ python any_iterable.py
['s', 'k', 'y', 'b', 'l', 'u', 'e']
[0, 0, 1, 0, 1, 2]
['apples', 'pears', 'plums']
[('apples', 4), ('pears', 2), ('plums', 7)]
Generator expressions
A starred generator expression produces the elements of each iterable one by one, without building an intermediate list.
def read_batches():
for batch in ([1, 2, 3], [4, 5], [6]):
print(f'reading batch {batch}')
yield batch
values = (*batch for batch in read_batches())
print(type(values).__name__)
print(next(values))
print(next(values))
print(sum(values))
The read_batches generator prints a message every time it
produces a batch, so we can see when each batch is read.
values = (*batch for batch in read_batches())
The expression creates a generator object; nothing is read yet. The generator
behaves like a function that runs yield from batch for each
batch.
print(next(values)) print(next(values)) print(sum(values))
The first next call reads the first batch and returns its first
element. The second call is served from the same batch. The
sum function consumes the rest of the elements, which reads the
remaining two batches. When a starred generator expression is the only
argument of a function call, the extra parentheses can be omitted, as in
sum(*b for b in batches).
$ python generator.py generator reading batch [1, 2, 3] 1 2 reading batch [4, 5] reading batch [6] 18
Asynchronous comprehensions
Unpacking also works in asynchronous comprehensions and asynchronous generator expressions.
import asyncio
async def fetch_pages():
for page in (['a', 'b'], ['c'], ['d', 'e']):
await asyncio.sleep(0.01)
yield page
async def main():
items = [*page async for page in fetch_pages()]
print(items)
gen = (*page async for page in fetch_pages())
print(type(gen).__name__)
print([item async for item in gen])
asyncio.run(main())
The fetch_pages asynchronous generator simulates a paginated API
that returns a list of items per page.
items = [*page async for page in fetch_pages()]
The async for clause awaits each page and its items are unpacked
into the list.
gen = (*page async for page in fetch_pages())
With parentheses, we get an asynchronous generator that produces the items lazily. It is consumed with another asynchronous comprehension.
$ python async_unpack.py ['a', 'b', 'c', 'd', 'e'] async_generator ['a', 'b', 'c', 'd', 'e']
Comparing with itertools.chain
Before Python 3.15, the standard tool for flattening was
itertools.chain.from_iterable.
from itertools import chain lists = [[1, 2], [3, 4], [5]] print([*xs for xs in lists]) print(list(chain.from_iterable(lists))) print([*xs for xs in lists if len(xs) > 1]) print(list(chain.from_iterable(xs for xs in lists if len(xs) > 1))) print([*reversed(xs) for xs in lists]) print(list(chain.from_iterable(reversed(xs) for xs in lists)))
The example computes the same results with unpacking and with
chain.from_iterable.
print([*xs for xs in lists if len(xs) > 1]) print(list(chain.from_iterable(xs for xs in lists if len(xs) > 1)))
For a plain flatten, the two forms are equally short. When we filter or
transform the pieces, chain.from_iterable needs a generator
expression as its argument, while the unpacking form keeps everything in one
comprehension. The chain function remains useful for code that
must run on older Python versions and when a lazy iterator is passed on to
other functions.
$ python chain_compare.py [1, 2, 3, 4, 5] [1, 2, 3, 4, 5] [1, 2, 3, 4] [1, 2, 3, 4] [2, 1, 4, 3, 5] [2, 1, 4, 3, 5]
Performance
The next example measures four ways of flattening one thousand lists of ten numbers.
from itertools import chain
from timeit import timeit
lists = [list(range(10)) for _ in range(1_000)]
tests = {
'unpacking': lambda: [*xs for xs in lists],
'nested loops': lambda: [e for xs in lists for e in xs],
'chain.from_iterable': lambda: list(chain.from_iterable(lists)),
'sum(lists, [])': lambda: sum(lists, []),
}
for label, fn in tests.items():
secs = timeit(fn, number=200)
print(f'{label:20} {secs * 1000:8.1f} ms')
Each approach runs 200 times with the timeit function. The exact
numbers depend on the machine.
'sum(lists, [])': lambda: sum(lists, []),
Adding lists with sum creates a new list on every addition, so
its running time grows quadratically. It should not be used for flattening.
On our machine, unpacking was the fastest approach, ahead of
chain.from_iterable and the nested loops.
$ python bench.py unpacking 4.9 ms nested loops 15.9 ms chain.from_iterable 11.6 ms sum(lists, []) 824.8 ms
Syntax errors
Some combinations of unpacking and comprehensions are not allowed. The following example compiles invalid snippets and prints the error messages.
snippets = [
'[**d for d in dicts]',
'[*a, *b for a, b in pairs]',
'[*x if x else [] for x in groups]',
'{*k: 1 for k in keys}',
]
for src in snippets:
try:
compile(src, '<snippet>', 'eval')
except SyntaxError as e:
print(f'{src:35} {e.msg}')
The compile function raises a SyntaxError for each
snippet; we print its msg attribute.
'[**d for d in dicts]', '[*a, *b for a, b in pairs]',
Dictionary unpacking is only allowed in dictionary comprehensions. The
expression part may contain only one starred item; to unpack two iterables
per step, use [*(*a, *b) for a, b in pairs] or
chain(a, b).
'[*x if x else [] for x in groups]',
'{*k: 1 for k in keys}',
A conditional expression must be wrapped in parentheses, and a dictionary key cannot be a starred expression. The error messages suggest the fix.
$ python syntax_errors.py
[**d for d in dicts] cannot use dict unpacking in list comprehension
[*a, *b for a, b in pairs] did you forget parentheses around the comprehension target?
[*x if x else [] for x in groups] invalid starred expression. Did you forget to wrap the conditional expression in parentheses?
{*k: 1 for k in keys} cannot use a starred expression in a dictionary key
Runtime errors
The values being unpacked are checked when the comprehension runs.
data = [[1, 2], 3]
try:
print([*x for x in data])
except TypeError as e:
print(e)
configs = [{'a': 1}, [('b', 2)]]
try:
print({**c for c in configs})
except TypeError as e:
print(e)
The second element of data is an integer, which cannot be
unpacked with a single star. The second element of configs is a
list of pairs, which is not a mapping, so it cannot be unpacked with a double
star. Both cases raise a TypeError.
$ python runtime_errors.py Value after * must be an iterable, not int 'list' object is not a mapping
Older Python versions
The syntax is new in Python 3.15. Running code that uses it on an older
version fails before any of the code runs. Here we run the
flatten.py example from the first section with Python 3.14.
Since the error is raised by the compiler, it cannot be caught with
try in the same module. Libraries that support older versions
have to keep using nested loops or itertools.chain.
$ python3.14 flatten.py
File "/home/user/python/flatten.py", line 3
flat = [*row for row in matrix]
^^^^
SyntaxError: iterable unpacking cannot be used in comprehension
Practical example
In the final example, we process a list of orders, each of which contains several line items.
orders = [
{'id': 1, 'items': [('pen', 2, 1.5), ('notebook', 1, 4.0)]},
{'id': 2, 'items': [('pen', 5, 1.5)]},
{'id': 3, 'items': [('stapler', 1, 12.0), ('notebook', 3, 4.0)]},
]
items = [*order['items'] for order in orders]
print(items)
total = sum(qty * price for _, qty, price in items)
print(f'{len(items)} line items, total {total:.2f}')
prices = {**{name: price for name, _, price in order['items']}
for order in orders}
print(prices)
Each line item is a tuple with a product name, a quantity, and a unit price.
items = [*order['items'] for order in orders]
We collect the line items of all orders into one list.
total = sum(qty * price for _, qty, price in items)
The total value is computed from the flat list with a regular generator expression.
prices = {**{name: price for name, _, price in order['items']}
for order in orders}
The inner dictionary comprehension maps product names to prices for one order. The outer comprehension merges these dictionaries into a price list covering all products.
$ python orders.py
[('pen', 2, 1.5), ('notebook', 1, 4.0), ('pen', 5, 1.5), ('stapler', 1, 12.0), ('notebook', 3, 4.0)]
5 line items, total 38.50
{'pen': 1.5, 'notebook': 4.0, 'stapler': 12.0}
Source
PEP 798 – Unpacking in Comprehensions
In this article we have shown how to use * and **
unpacking in comprehensions and generator expressions in Python 3.15. See also
Python list comprehensions and
Python chain function.
Author
List all Python tutorials.