Python Iterable Unpacking: Syntax and Pitfalls
python iterable unpacking: Understand Python iterable unpacking: basic syntax, starred expressions, loops, function calls, and common pitfalls for cleaner code.
Python iterable unpacking lets you assign multiple variables from an iterable in a single statement. It is a core feature of the language that appears in everyday code, from swapping values to parsing function return values. This article covers the syntax, advanced patterns, and the edge cases that trip up even experienced developers.
The Basic Unpacking Syntax
The simplest form of unpacking assigns each element of an iterable to a variable in order. The number of variables must match the number of elements exactly.
coordinates = (10, 20) x, y = coordinates print(x, y) # 10 20
This works with any iterable, not just tuples. Lists, strings, and generator objects are all valid sources.
first, second = [1, 2] letter1, letter2 = "ab"
When the iterable is longer than the number of variables, Python raises a ValueError with the message "too many values to unpack". Conversely, if there are fewer elements, it raises "not enough values to unpack". This strictness is intentional: it forces you to handle the data shape explicitly rather than silently discarding or inventing values.
A common use is swapping variables without a temporary variable:
a, b = b, a
The right side is evaluated first, creating a tuple, then unpacked into the left side. This is idiomatic Python and avoids the need for a third variable.
Extended Unpacking with Starred Expressions
Python 3 introduced extended unpacking, which allows a single starred variable to absorb a sequence of elements. The starred expression can appear anywhere in the assignment target list.
first, *rest = [1, 2, 3, 4] print(first) # 1 print(rest) # [2, 3, 4]
*init, last = [1, 2, 3, 4] print(init) # [1, 2, 3] print(last) # 4
The starred variable always receives a list, even if the source is a tuple or string. This is a deliberate design choice to keep the result uniform.
Extended unpacking is particularly useful when you need to separate the first element from the rest, or the last element from the prefix. It also works with any iterable, including generators, but note that it will consume the entire generator to build the list for the starred variable.
You can combine a starred expression with fixed variables, but only one starred expression is allowed per assignment. Attempting to use two will raise a SyntaxError.
Unpacking in Loops and Function Calls
Iterable unpacking shines in loops. Instead of indexing into a list of pairs, you can unpack each item directly in the for statement.
pairs = [(1, 'one'), (2, 'two')] for number, name in pairs: print(f"{number}: {name}")
This works for any iterable of iterables, such as dictionaries when iterating over .items():
d = {'a': 1, 'b': 2} for key, value in d.items(): print(key, value)
Function calls also use unpacking. The * operator unpacks an iterable into positional arguments, and ** unpacks a mapping into keyword arguments.
def add(x, y): return x + y args = (3, 4) print(add(*args)) # 7
def greet(name, greeting="Hello"): print(f"{greeting}, {name}") kwargs = {"name": "Alice", "greeting": "Hi"} greet(**kwargs) # Hi, Alice
These operators are distinct from the starred assignment unpacking, but they share the same mental model: expanding an iterable into individual components.
Unpacking Dictionaries and Mapping Types
Dictionaries are iterable over their keys, so unpacking a dictionary directly gives keys, not key-value pairs.
d = {'x': 1, 'y': 2} a, b = d print(a, b) # x y
To unpack keys and values together, use .items():
(k1, v1), (k2, v2) = d.items()
But that is brittle because it assumes exactly two items. A more robust approach is to loop over .items().
In function calls, ** unpacks a dictionary into keyword arguments. This is common when passing configuration dictionaries to functions.
config = {'host': 'localhost', 'port': 8080} connect(**config)
Python 3.5 also introduced dictionary unpacking in dictionary literals, allowing you to merge mappings:
base = {'a': 1} extra = {'b': 2} merged = {**base, **extra}
This is a concise way to combine dictionaries, though it creates a new dictionary and does not mutate the originals.
Common Mistakes and Edge Cases
One frequent error is assuming that unpacking a string gives individual characters as separate variables. It does, but only if the string length matches the number of variables. If you need to split on a delimiter, use .split() instead.
Another pitfall is unpacking a generator that is infinite or very large. When a starred expression is present, Python will consume the entire generator to build the list, which can exhaust memory. For example:
first, *rest = generate_numbers() # consumes all
If you only need a few elements, consider using itertools.islice to avoid materializing the whole sequence.
Nested unpacking is allowed but can become hard to read. For instance:
(a, (b, c)) = (1, (2, 3))
This works, but if the nested structure is complex, it may be clearer to unpack in steps.
A subtle bug occurs when you use a starred expression in a loop that expects a fixed number of elements. The starred variable will capture all remaining items, which may hide data shape issues. Always verify that the iterable has the expected structure.
Runtime and Memory Considerations
Unpacking itself is a low-cost operation. For simple assignment, Python creates a tuple from the right-hand side if it is not already one, then assigns each element. This is O(n) in the number of variables, and the overhead is negligible for typical sizes.
However, extended unpacking with a starred variable can have memory implications. The starred variable always becomes a list, so if the source is a large iterable, that list occupies memory proportional to its length. This is often fine, but it contradicts the lazy nature of generators. If you are unpacking a generator that yields millions of items, the starred expression will force full evaluation, which can cause a MemoryError.
For performance-critical code, consider whether you can avoid unpacking a large iterable entirely. For example, if you only need the first few items, use next() on an iterator instead of unpacking with a star.
Another consideration is that unpacking in a for loop creates a new tuple for each iteration if the iterable yields tuples. This is usually acceptable, but if you are iterating over millions of records, the overhead of tuple creation might be measurable. In such cases, indexing into a pre-built list might be slightly faster, but the difference is often negligible compared to the actual work done in the loop body.
When to Use Unpacking vs. Indexing
Choosing between unpacking and indexing depends on the context and the readability you need.
Use unpacking when:
- The structure of the iterable is fixed and known in advance.
- You want to give meaningful names to each element.
- You are iterating over a collection of pairs or tuples.
- You need to swap values or assign multiple variables at once.
Use indexing when:
- You only need one or two elements from a larger collection.
- The iterable is a list or tuple with a variable length, and you want to access by position.
- You are working with a sequence where the position is semantically meaningful (e.g.,
coordinates[0]).
Indexing is also more appropriate when the iterable is a string and you want to access characters by position, though unpacking a string into a fixed number of variables is possible if the length is guaranteed.
In function calls, *args and **kwargs are the standard way to forward arguments, and they are preferable to manually indexing into a list of arguments because they preserve the function signature.
A practical rule: if unpacking makes the code read like the data structure it represents, use it. If it obscures the relationship between positions and meaning, stick with indexing or explicit variable assignment.
Unpacking is not a performance tool; it is a readability tool. The Python interpreter optimizes simple unpacking well, but the real benefit is that it communicates intent clearly and reduces the chance of off-by-one errors when accessing elements by index.