
Unpacking in Python: Starred Expressions, Swaps, and Multiple Assignment
Unpacking is one of those Python features you use long before you know its name. for key, value in d.items(): is unpacking. So is x, y = point, and so is a, b = b, a. It's everywhere because it lets you pull values out of a structure and give each one a meaningful name in a single step, instead of indexing with [0], [1], and [2] and hoping you remember which is which.
Underneath, it's one general mechanism with a few extensions: starred targets that collect "the rest", nested patterns that mirror nested data, and the * and ** operators that spread iterables and dicts into new collections or function calls.
This post covers how multiple assignment actually works, the swap idiom and why it's safe, starred expressions, nested unpacking, unpacking in loops, the _ convention for values you don't need, and unpacking inside literals and calls. It also points out the error messages you'll see when the shapes don't match.
The Basics: Multiple Assignment
When the left side of an assignment has several comma-separated names, Python treats the right side as an iterable and assigns its items to the names in order:
point = (3, 4)
x, y = point
print(x, y)
3 4
The right side doesn't have to be a tuple. Any iterable works: lists, strings, ranges, generators, files, even dicts (which iterate over their keys):
a, b, c = "abc"
print(a, b, c)
k1, k2 = {"one": 1, "two": 2}
print(k1, k2)
a b c
one two
The number of names must match the number of items exactly. If it doesn't, you get a ValueError that tells you which way it's off:
x, y = (1, 2, 3)
ValueError: too many values to unpack (expected 2)
x, y, z = (1, 2)
ValueError: not enough values to unpack (expected 3, got 2)
That strictness is a feature. If a function you expected to return two values starts returning three, unpacking fails loudly instead of silently ignoring the extra one.
Parentheses and Brackets Are Optional
The comma makes the tuple, not the parentheses. These are all equivalent ways to write the left side:
x, y = 1, 2
(x, y) = 1, 2
[x, y] = 1, 2
On the right side, 1, 2 is a tuple literal. That's also why t = 1, creates a one-element tuple, a trailing comma that's easy to add by accident:
t = 1,
print(t, type(t))
(1,) <class 'tuple'>
Swapping Without a Temporary Variable
The classic demonstration of unpacking is swapping two variables:
a, b = 1, 2
a, b = b, a
print(a, b)
2 1
This works because Python evaluates the entire right side first. b, a builds a tuple (2, 1) from the current values, and only then are the names on the left rebound. No temporary variable needed, because the tuple is the temporary.
The same idea works for list elements:
nums = [10, 20, 30]
nums[0], nums[2] = nums[2], nums[0]
print(nums)
[30, 20, 10]
And for updating several values from their old state at once, as in this Fibonacci loop:
a, b = 0, 1
for _ in range(6):
a, b = b, a + b
print(a)
8
Without simultaneous assignment you'd need a temp variable to avoid using the new a when computing the new b.
Assignment Order on the Left
The right side is evaluated in full first, but the targets on the left are assigned left to right, one at a time. Usually that doesn't matter. It does if a later target depends on an earlier one:
i = 0
items = [0, 0]
i, items[i] = 1, 5
print(items)
[0, 5]
i is set to 1 first, so items[i] refers to items[1], not items[0]. Code that depends on this is confusing to read; split it into two statements if you ever find yourself writing it.
Chained Assignment Is Different
Don't confuse unpacking with chained assignment. x = y = [] binds both names to the same object:
x = y = []
x.append(1)
print(y)
[1]
That's aliasing, not unpacking. If you want two separate lists, write x, y = [], []. Python Variables and Mutability goes deeper on why this happens.
Starred Targets: Collecting the Rest
PEP 3132 added starred assignment targets. A name prefixed with * collects every item not assigned to the other names, always as a list:
first, *rest = [1, 2, 3, 4]
print(first, rest)
*init, last = [1, 2, 3, 4]
print(init, last)
head, *middle, tail = "python"
print(head, middle, tail)
1 [2, 3, 4]
[1, 2, 3] 4
p ['y', 't', 'h', 'o'] n
Notice that even when unpacking a string or tuple, the starred name gets a list.
A few rules:
-
Only one starred target per level. Python can't decide how to split items between two of them.
*a, *b = [1, 2, 3]SyntaxError: multiple starred expressions in assignment -
The starred target can be empty. The other names still need items, but the star accepts zero:
a, *b = (1,) print(a, b)1 [] -
A star alone needs a trailing comma.
*b, = range(3)works and gives[0, 1, 2], thoughlist(range(3))is clearer.
Where Starred Targets Shine
Starred targets fit naturally anywhere data has a fixed "front" and a variable "rest":
lines = ["header", "r1", "r2"]
header, *rows = lines
print(header, rows)
cmd, *args = "git commit -m msg".split()
print(cmd, args)
header ['r1', 'r2']
git ['commit', '-m', 'msg']
Compare that with header = lines[0] and rows = lines[1:]: the unpacking version states the structure once and names both parts.
With an iterator, you can grab the first few values and discard the rest with *_:
it = iter(range(5))
first, second, *_ = it
print(first, second)
0 1
Be careful with that on large or infinite iterators: *_ still consumes everything to build its list. To take only the first few items from a big iterator, use itertools.islice():
import itertools
x, y = itertools.islice(range(1_000_000), 2)
print(x, y)
0 1
Nested Unpacking
When the data is nested, the targets can be nested too. The left side mirrors the shape of the right:
record = ("Ada", (1815, 12, 10))
name, (year, month, day) = record
print(name, year)
Ada 1815
(a, b), c = [1, 2], 3
print(a, b, c)
1 2 3
Nesting is most useful in loops, which is where we're headed next. If you need to match on the shape rather than just assume it (different shapes, type checks, literal values), that's the job of match statements, covered in Structural Pattern Matching in Python.
Unpacking in for Loops
The target of a for loop is an assignment target, so everything above applies to it:
pairs = [("a", 1), ("b", 2)]
for letter, num in pairs:
print(letter, num)
a 1
b 2
Combine it with enumerate() and you'll need nested unpacking, because enumerate() yields (index, item) pairs and each item is itself a pair:
for i, (letter, num) in enumerate(pairs):
print(i, letter, num)
0 a 1
1 b 2
Iterating over a dict with .items() is the most common unpacking loop of all:
prices = {"mug": 12, "lamp": 65}
for name, price in prices.items():
print(f"{name}: {price}")
mug: 12
lamp: 65
enumerate(), zip(), and friends are covered in detail in Python's enumerate, zip, and range.
Returning Multiple Values
Python functions return one object, but that object can be a tuple, and unpacking makes it feel like multiple return values:
def stats(values: list[float]) -> tuple[float, float]:
return min(values), max(values)
lo, hi = stats([3, 1, 4])
print(lo, hi)
1 4
The return min(values), max(values) line builds a tuple; the caller unpacks it. This is idiomatic for two or three closely related values. Beyond that, positional results become hard to keep straight, and a NamedTuple or dataclass with named fields is a better return type.
NamedTuple instances still unpack like plain tuples, so switching is backward compatible. Dataclasses don't unpack by default:
from dataclasses import astuple, dataclass
@dataclass
class Pt:
x: int
y: int
qx, qy = Pt(1, 2)
TypeError: cannot unpack non-iterable Pt object
You can convert with dataclasses.astuple() (qx, qy = astuple(Pt(1, 2))), though if you're unpacking a dataclass often, it may be a sign that a tuple-like type fits better.
Ignoring Values with _
When you only need some of the values, the convention is to assign the rest to _:
user, _, domain = "ada@example.com".partition("@")
print(user, domain)
filename, _, ext = "report.final.pdf".rpartition(".")
print(filename, ext)
ada example.com
report.final pdf
_ is a perfectly ordinary variable name; Python doesn't treat it specially in assignments. The underscore is just a signal to readers and linters that the value is intentionally unused. Combine it with a star (*_) to ignore any number of values.
One caution: in the interactive interpreter, _ holds the last result, and some projects use _ as an alias for a translation function like gettext. If yours does, pick another throwaway name such as _unused.
Unpacking Strings with split()
Splitting a string and unpacking the result is a quick parser for simple formats:
row = "ada,36,London"
name, age, city = row.split(",")
print(name, int(age), city)
key, value = "timeout=30".split("=", 1)
print(key, value)
ada 36 London
timeout 30
The maxsplit argument of 1 in the second example matters: without it, a value containing = would produce extra pieces and the unpacking would fail. The strictness of unpacking is working for you here, too: a malformed line raises ValueError instead of producing a half-filled record. For real CSV, use the csv module, which handles quoting.
Spreading with * and **
The same star syntax also works in the other direction: spreading an iterable's items into a new collection or a function call. PEP 448 generalized this so it works in literals.
In List, Tuple, and Set Literals
print([*range(3), *"ab"])
print({*[1, 2], *[2, 3]})
[0, 1, 2, 'a', 'b']
{1, 2, 3}
You can mix spread iterables with regular elements in any order: [0, *middle, 99] is a clean way to build a list around existing items.
In Dict Literals
** spreads a mapping's key/value pairs. Later keys override earlier ones, which makes it a natural way to apply overrides on top of defaults:
defaults = {"theme": "light", "lang": "en"}
overrides = {"theme": "dark"}
print({**defaults, **overrides})
print(defaults | overrides)
{'theme': 'dark', 'lang': 'en'}
{'theme': 'dark', 'lang': 'en'}
Since Python 3.9, the | operator merges dicts too and is often more readable. The ** form is still handy when you want to add literal keys in the same expression: {**defaults, "debug": True}.
In Function Calls
In a call, * spreads an iterable into positional arguments, and ** spreads a dict into keyword arguments:
def area(w: float, h: float) -> float:
return w * h
dims = (3, 4)
print(area(*dims))
print(area(**{"w": 2, "h": 5}))
print(*[1, 2, 3], sep=", ")
12
10
1, 2, 3
The print(*items, sep=", ") trick prints the items of a list as separate arguments, which is a quick way to join non-string values. The flip side, defining functions that accept a variable number of arguments with *args and **kwargs, is its own topic: see *args and **kwargs in Python.
Common Errors and What They Mean
| Error | Cause |
|---|---|
ValueError: too many values to unpack (expected 2) | The iterable has more items than targets. Add a starred target or check the data. |
ValueError: not enough values to unpack (expected 3, got 2) | Fewer items than targets. Often a malformed input line. |
TypeError: cannot unpack non-iterable int object | The right side isn't iterable, often a function returning a single value or None. |
SyntaxError: multiple starred expressions in assignment | Two * targets at the same level. |
The TypeError with NoneType is worth calling out: if you see cannot unpack non-iterable NoneType object, a function you expected to return a tuple returned None, usually because some code path fell off the end without a return.
Conclusion
Unpacking binds several names from one iterable in a single statement, and Python enforces that the shapes match. That gives you readable code (name, age, city = row instead of three index lookups) and early, explicit errors when the data isn't what you assumed. The swap idiom works because the right side is evaluated in full before anything is assigned; starred targets collect "the rest" into a list; nested targets mirror nested data; and * and ** spread iterables and mappings into literals and calls.
Whenever you catch yourself writing thing[0], thing[1], and thing[2] on consecutive lines, that's a good sign the code would read better as a single unpacking assignment.


