Python Starred Unpacking: Syntax, Uses, and Pitfalls
python starred unpacking: Learn how Python's starred unpacking works for sequences, function calls, and definitions, including common pitfalls and performance consider...
Python's starred unpacking syntax lets you expand iterables and mappings in assignments and function calls. It appears as a single asterisk (*) for sequences and a double asterisk (**) for mappings. This article explains how python starred unpacking works, where it applies, and the mistakes that trip developers.
The Basic Unpacking Operator
The simplest form of starred unpacking appears in assignment statements. When you write a, b = [1, 2], Python unpacks the list into two variables. The star extends this to capture an arbitrary number of elements:
first, *middle, last = [1, 2, 3, 4, 5] print(first) # 1 print(middle) # [2, 3, 4] print(last) # 5
The starred target collects zero or more elements into a list. It can appear at most once per assignment, but it can be placed anywhere among the targets. This works with any iterable, not just lists:
head, *tail = (10, 20, 30) print(head) # 10 print(tail) # [20, 30]
A common use is splitting a sequence into its first element and the remainder, which is useful in recursive algorithms or when processing command-line arguments.
Unpacking in Function Calls
Starred unpacking also expands iterables when calling functions. Instead of passing a list as a single argument, you can spread its elements into separate positional parameters:
def add(a, b, c): return a + b + c values = [1, 2, 3] result = add(*values) print(result) # 6
The double star does the same for keyword arguments, but it expects a mapping whose keys match the parameter names:
def describe(name, age): return f"{name} is {age} years old" info = {"name": "Ada", "age": 36} print(describe(**info)) # Ada is 36 years old
This pattern is common when delegating to another function or when building calls dynamically. It also works with built-in functions. For example, zip(*pairs) transposes a list of pairs, and print(*items) prints each item as a separate argument.
Extended Unpacking in Assignments
Beyond simple splitting, starred unpacking supports more complex patterns. You can use it to swap elements, flatten nested structures, or parse fixed-width records:
record = ("John", "Doe", 1985, "NY") name, *_, year, city = record print(name) # John print(year) # 1985
The underscore _ is a conventional placeholder for values you intend to ignore. This is useful when you only need a few fields from a tuple or list.
Starred unpacking also works in for-loop targets. Iterating over a list of tuples and unpacking each one is a standard pattern:
points = [(1, 2), (3, 4), (5, 6)] for x, y in points: print(x + y)
You can combine this with a star to capture variable-length segments inside each iteration.
Starred Unpacking in Function Definitions
In function definitions, *args collects extra positional arguments into a tuple, and **kwargs collects extra keyword arguments into a dictionary. This is the inverse of unpacking in calls:
def log(message, *args, **kwargs): print(message) print("args:", args) print("kwargs:", kwargs) log("start", 1, 2, level="info") # args: (1, 2) # kwargs: {'level': 'info'}
These parameters are often used for wrappers, decorators, or APIs that need to accept arbitrary arguments. A bare * in a definition forces all following parameters to be keyword-only:
def compare(a, b, *, key=None): pass
This improves readability and prevents accidental positional misuse.
Common Mistakes and Edge Cases
One frequent error is using a star with a generator that can be exhausted only once. Unpacking consumes the generator, so you cannot reuse it later. For example:
gen = (x for x in range(3)) a, b, c = gen print(list(gen)) # []
Another mistake is attempting to unpack a mapping with a single star. A single star on a dictionary yields its keys, not its values:
d = {"x": 1, "y": 2} print(*d) # x y
To unpack values, use *d.values() or **d for keyword arguments.
Starred unpacking requires at least one non-starred target when the iterable is empty. For example, first, *rest = [] raises ValueError: not enough values to unpack. This is a runtime error, not a syntax error, so it can appear in production if input is not validated.
Performance and Memory Considerations
Starred unpacking creates a new list for the starred target. If you unpack a very large iterable, the collected elements are materialized in memory. For instance, first, *rest = huge_list copies all elements except the first into a new list, doubling memory usage temporarily. This is usually acceptable, but for massive data, consider using an iterator or slicing to avoid the copy.
Function calls with *args also allocate a tuple to hold the collected arguments. The overhead is small for typical call counts, but in tight loops with millions of calls, it can add up. In such cases, passing a single iterable and iterating inside the function may be more efficient.
There is no inherent performance penalty for using **kwargs; it is a dictionary lookup at call time. However, building a dictionary just to unpack it adds allocation cost. Prefer explicit keyword arguments when the set is known at compile time.
Maintainability and Readability Tradeoffs
Starred unpacking can make code more concise, but it can also obscure intent. Using *args and **kwargs in a public API hides the actual signature, making it harder for callers to know what arguments are expected. This is acceptable for wrappers and decorators, but for core functions, explicit parameters are usually clearer.
Similarly, overusing extended unpacking in assignments can reduce readability. A pattern like a, *b, c = data is fine when the structure is well-known, but if the data format changes, the unpacking will silently produce wrong results or raise errors. Add type hints or comments to document the expected shape.
When you need to ignore many elements, consider using an index or slicing instead of a star, especially if the ignored portion is large. The star always creates a list, even if you discard it, which wastes memory.
Starred Unpacking with Custom Iterables
Any object that implements __iter__ can be unpacked. This includes custom classes, generators, and file objects. When you unpack a file object line by line, the star will consume the entire file into a list, which is rarely desirable. Prefer iterating directly instead of unpacking.
For mappings, ** works with any object that has keys() and __getitem__, but the standard use is with dictionaries. If you need to merge dictionaries, {**d1, **d2} is a concise idiom that creates a new dictionary. This is often clearer than d1.update(d2) because it does not mutate the original.
Be aware that starred unpacking in a dictionary literal is supported from Python 3.5 onward. In earlier versions, you would need dict(d1, **d2) or a loop. This version dependency matters if you support legacy environments.
Choosing the Right Unpacking Pattern
The decision between *args, **kwargs, and explicit parameters depends on the stability of the interface. If the function is internal and the argument set is fixed, use explicit names. If you are writing a decorator or a generic dispatch layer, *args and **kwargs are necessary.
For assignment unpacking, use a star when the number of elements varies and you need the remainder. Use fixed targets when the structure is guaranteed. For example, parsing a two-element tuple like (x, y) does not need a star.
When performance matters, measure the actual bottleneck. Starred unpacking is rarely the cause of slow code, but if it appears in a hot loop, consider rewriting the loop to avoid allocation. The standard library's itertools module offers alternatives like islice for partial consumption without copying.
Ultimately, python starred unpacking is a versatile feature that, used with discipline, improves code clarity and reduces boilerplate. The key is to match the pattern to the data's actual variability and to document the expected structure so future maintainers can follow the logic.