Skip to main content Brad's PyNotes

PEP 448: Additional Unpacking Generalizations

TL;DR

PEP 448 extended Python’s unpacking operators (* and **) in Python 3.5, allowing multiple unpackings in function calls and enabling unpacking directly within list, tuple, set, and dictionary literals. This eliminates verbose workarounds and makes code more concise and readable.

Interesting!

Before PEP 448, you couldn’t unpack multiple iterables in a single list literal. Creating [*a, *b, *c] required awkward workarounds like list(itertools.chain(a, b, c)) or manual concatenation. PEP 448 removed these arbitrary restrictions, making unpacking work naturally wherever it makes sense.

Multiple Unpacking in Function Calls

Python 3.5 allows multiple * and ** unpackings in function calls, eliminating the need for intermediate variables:

python code snippet start

# Multiple * unpackings
def greet(a, b, c, d):
    print(f"{a}, {b}, {c}, and {d}")

first = [1, 2]
second = [3, 4]
greet(*first, *second)  # 1, 2, 3, and 4

# Before PEP 448, this required:
combined = first + second
greet(*combined)

# Multiple ** unpackings
def configure(host, port, timeout, retries):
    print(f"{host}:{port}, timeout={timeout}, retries={retries}")

defaults = {'timeout': 30, 'retries': 3}
overrides = {'port': 8080}
configure(host='localhost', **defaults, **overrides)
# localhost:8080, timeout=30, retries=3

python code snippet end

The order matters: later values override earlier ones with ** unpacking.

Unpacking in Collection Literals

The most powerful feature is unpacking directly within list, tuple, set, and dictionary literals:

python code snippet start

# List unpacking
a = [1, 2, 3]
b = [4, 5]
combined = [*a, *b, 6, 7]
print(combined)  # [1, 2, 3, 4, 5, 6, 7]

# Before PEP 448:
combined = a + b + [6, 7]
# Or: combined = list(itertools.chain(a, b, [6, 7]))

# Tuple unpacking
colors = ('red', 'green')
more_colors = (*colors, 'blue', 'yellow')
print(more_colors)  # ('red', 'green', 'blue', 'yellow')

# Set unpacking
set1 = {1, 2, 3}
set2 = {3, 4, 5}
merged = {*set1, *set2, 6}
print(merged)  # {1, 2, 3, 4, 5, 6}

python code snippet end

Dictionary Merging Made Easy

Merging dictionaries becomes remarkably clean with ** unpacking:

python code snippet start

# Dictionary unpacking
base_config = {'host': 'localhost', 'port': 8000}
user_config = {'port': 9000, 'debug': True}

config = {**base_config, **user_config, 'timeout': 30}
print(config)
# {'host': 'localhost', 'port': 9000, 'debug': True, 'timeout': 30}

# Before PEP 448:
config = base_config.copy()
config.update(user_config)
config['timeout'] = 30

# Or using ChainMap (less intuitive):
from collections import ChainMap
config = dict(ChainMap({'timeout': 30}, user_config, base_config))

python code snippet end

Later keys override earlier ones, so user_config['port'] wins over base_config['port'].

Practical Use Cases

Combining Data Sources

python code snippet start

# Merge multiple API responses
api_data_1 = {'users': 10, 'posts': 50}
api_data_2 = {'comments': 200, 'likes': 1500}
api_data_3 = {'shares': 75}

dashboard = {**api_data_1, **api_data_2, **api_data_3}
# {'users': 10, 'posts': 50, 'comments': 200, 'likes': 1500, 'shares': 75}

# Combine search results from multiple sources
results_db = ['result1', 'result2']
results_cache = ['result3', 'result4']
results_api = ['result5']

all_results = [*results_db, *results_cache, *results_api]

python code snippet end

Building Complex Configurations

python code snippet start

# Application configuration layers
system_defaults = {'log_level': 'INFO', 'workers': 4}
environment_config = {'workers': 8}
user_overrides = {'log_level': 'DEBUG'}

# Merge with clear precedence: defaults < environment < user
final_config = {
    **system_defaults,
    **environment_config,
    **user_overrides
}
print(final_config)
# {'log_level': 'DEBUG', 'workers': 8}

python code snippet end

Flexible Function Arguments

python code snippet start

def process_items(*items, verbose=False):
    if verbose:
        print(f"Processing {len(items)} items")
    return sum(items)

# Combine different sources
database_values = [1, 2, 3]
cache_values = [4, 5]
default_value = [0]

result = process_items(*default_value, *database_values, *cache_values, verbose=True)
# Processing 6 items
print(result)  # 15

python code snippet end

Flattening Nested Structures

python code snippet start

# Flatten nested lists
nested = [[1, 2], [3, 4], [5, 6]]
flat = [*nested[0], *nested[1], *nested[2]]
print(flat)  # [1, 2, 3, 4, 5, 6]

# More generally:
from functools import reduce
flat = reduce(lambda acc, lst: [*acc, *lst], nested, [])

# Add headers and footers
header = ['ID', 'Name', 'Email']
data_row = ['001', 'Alice', 'alice@example.com']
footer = ['---', '---', '---']

table_row = [*header, *data_row, *footer]

python code snippet end

Important Limitations

Comprehensions Not Supported

Unpacking operators don’t work inside comprehensions:

python code snippet start

# This does NOT work:
# result = [*x for x in lists]  # SyntaxError

# Instead, use explicit iteration:
result = [item for sublist in lists for item in sublist]

# Or use unpacking outside the comprehension:
result = [*itertools.chain.from_iterable(lists)]

python code snippet end

Dictionary Key Conflicts

When unpacking dictionaries, duplicate keys follow last-wins semantics:

python code snippet start

dict1 = {'x': 1, 'y': 2}
dict2 = {'y': 3, 'z': 4}

merged = {**dict1, **dict2}
print(merged)  # {'x': 1, 'y': 3, 'z': 4} - dict2's 'y' wins

# Function calls with ** raise TypeError on duplicates
def func(x, y):
    return x + y

# This raises TypeError at runtime:
# func(**{'x': 1}, **{'x': 2})  # TypeError: multiple values for 'x'

python code snippet end

Migration from Old Code

Refactoring to use PEP 448 improves readability:

python code snippet start

# Before PEP 448
def build_request(base_headers, auth_headers, custom_headers):
    headers = {}
    headers.update(base_headers)
    headers.update(auth_headers)
    headers.update(custom_headers)
    return headers

# After PEP 448
def build_request(base_headers, auth_headers, custom_headers):
    return {**base_headers, **auth_headers, **custom_headers}

# Before PEP 448
def combine_results(results1, results2, extra_items):
    return list(itertools.chain(results1, results2, extra_items))

# After PEP 448
def combine_results(results1, results2, extra_items):
    return [*results1, *results2, *extra_items]

python code snippet end

PEP 448 removed arbitrary restrictions on unpacking, making Python’s unpacking syntax more consistent and powerful across all contexts where it makes sense. For another concise syntax that transforms how you create collections, see dictionary comprehensions .

Reference: PEP 448 - Additional Unpacking Generalizations