Skip to content
← All Python articles

Python explainer

Flatten and Merge with Unpacking in Comprehensions

About 4 min to read. Allow extra time to work through examples.

Python 3.15 lets you write * and ** inside a comprehension to flatten lists and merge dictionaries. See where the star goes, the trap with function calls, and what to write on older versions.

This article uses Python. You can make it your practice mode to browse related content across the site.

You have each blog post's tags as a list, and you want all the tags in one list:

tag_lists = [["python", "web"], [], ["web", "testing"]]

Python 3.15 lets you unpack inside a comprehension. *tags adds each item of tags to the result, as it does in a list display such as [*a, *b]:

all_tags = [*tags for tags in tag_lists]
print(all_tags)   # ['python', 'web', 'web', 'testing']

Before 3.15 you write two for clauses, which run left to right like two nested for loops:

all_tags = [tag for tags in tag_lists for tag in tags]

Both give the same list. The new form says "the items of each tags", and it needs no name for the inner item.

Sets and dictionaries work too

* works the same way in a set comprehension, and ** merges dictionaries in a dict comprehension:

unique_tags = {*tags for tags in tag_lists}
# {'python', 'web', 'testing'}

layers = [{"host": "localhost", "port": 8000}, {"port": 9000}]
config = {**layer for layer in layers}
# {'host': 'localhost', 'port': 9000}
  • The list keeps every item, in order, duplicates included.
  • The set drops duplicates. Its items must be hashable, and its order is not reliable.
  • The dict merges the mappings in order. A later value replaces an earlier one, and the key keeps the position where it first appeared.

Unpacking flattens one level only. [*row for row in [[1, [2, 3]], [4]]] gives [1, [2, 3], 4].

With parentheses instead of brackets, you get a generator, which produces the items one at a time without building a list. So sum(*batch for batch in batches) adds up every number in every batch.

Where the star goes changes the result

groups = [[1, 2], [3]]
[*group for group in groups]      # [1, 2, 3], one flat list
[(*group,) for group in groups]   # [(1, 2), (3,)], one tuple per group

Two other mistakes give a clear SyntaxError:

  • ** works only in a dict comprehension. [**layer for layer in layers] fails with "cannot use dict unpacking in list comprehension".
  • To unpack a conditional expression, wrap it in parentheses, as in [*(a if use_a else b) for a, b in pairs].
Check your understanding With batches = [[3, 4], [5]], what does print(*batch for batch in batches) print on Python 3.15? Show answer

It prints a generator object, such as <generator object <genexpr> at 0x...>, and not 3 4 5. The star belongs to the generator expression, so print gets one argument, the generator. sum(*batch for batch in batches) works for the same reason, because sum takes one iterable. To pass every item as a separate argument, put the generator in a variable first, then call print(*items).

On older versions

Python 3.14 and older reject the new form with SyntaxError: iterable unpacking cannot be used in comprehension. A version check in the same file cannot hide it, because Python reads the whole file before it runs any of it. Write Code That Runs on Several Python Versions explains your choices. To support older versions, use two for clauses, or itertools.chain.from_iterable(tag_lists), which produces the items one at a time like the generator form.

This site runs exercise code on your machine, or on a machine you linked, with the Python that the runtime there uses. Use the new form in an exercise only if that Python is 3.15 or newer.

Check your understanding Your project now requires Python 3.15. Should you replace every chain.from_iterable call with the new form? Show answer

Only where it reads better. Both forms are correct, so a blanket rewrite changes many lines and fixes nothing. If you do switch, keep producing the items one at a time. chain.from_iterable(groups) produces items one at a time, so its match is the generator (*g for g in groups), not the list [*g for g in groups].

Going further

The change comes from PEP 798. Async comprehensions support it too, as in [*chunk async for chunk in stream], where stream is an async iterator that yields lists.

step 20 of 23

Pattern Matching, f-Strings and Newer Syntax

Up next Speed Up Startup with Lazy Imports