Tuple packing and unpacking is the feature that makes tuples feel almost invisible in idiomatic Python. Writing point = 12.5, 47.8 packs two numbers into a tuple, and writing name, price = product pulls a tuple apart into named variables. This chapter covers every pattern you'll meet in real code: writing tuples without parentheses, matching arity, the swap idiom, starred targets, ignoring values, unpacking function returns, and unpacking inside loops with enumerate and zip.
Packing is how Python builds a tuple from a comma-separated list of values. The parentheses are not what make a tuple; the comma is.
No parentheses, no tuple() call, just two values and a comma. Python reads the comma as "build a tuple", and point ends up bound to a two-element tuple. The interactive output still shows parentheses because that's how repr chooses to display tuples; the parentheses are a print-time convenience, not a syntax requirement.
You can pack any number of values this way:
Three values separated by commas produce a three-element tuple. The names of the variables on each side of the comma don't matter; only the commas do. Parentheses are a grouping tool, not a tuple constructor, so they're optional in most contexts.
There's one place where parentheses are genuinely required: inside another expression where the comma would mean something else. A function call, for example, uses commas to separate arguments, so a bare tuple inside a call needs parentheses to avoid being misread as several arguments.
Without the inner parentheses, describe(12.5, 47.8) would call describe with two arguments and raise TypeError. When in doubt, add the parentheses. They never hurt readability and they protect against ambiguity.
The singleton tuple is where the comma rule bites. A value in parentheses is just that value. To pack a single value into a tuple, you need a trailing comma.
Without the trailing comma, the parentheses are just grouping. With the trailing comma, Python sees a one-element tuple. Same rule as before, restated: the comma builds the tuple.
Unpacking is the reverse direction. Put a tuple on the right and a comma-separated list of names on the left, and Python binds each name to the element at the matching position.
Three names on the left, three elements on the right. The first name takes the first element, the second name takes the second, and so on. Parentheses on the left are optional too; name, price, stock = product is the same as (name, price, stock) = product.
The rule for basic unpacking is strict: the count of names on the left must equal the count of elements on the right. Anything else raises ValueError.
Two names on the left, three elements on the right. Python won't drop the third value silently. The error names the expected count, which is the count on the left side of the equals sign.
The same error appears in the other direction when there aren't enough values:
Three names on the left, two elements on the right. Python tells you exactly what was expected and what was found. This strictness is intentional. If your code expects a tuple of (name, price, stock) and gets only (name, price), you almost certainly want to know loudly rather than silently bind stock to nothing and crash a hundred lines later.
A subtle pitfall: strings are iterable, so unpacking a string treats each character as one element. If you write a, b, c = product_code thinking product_code is a three-tuple but it turns out to be the string "ABC", the unpacking succeeds and binds a="A", b="B", c="C".
The single line a, b = b, a swaps two variables without a temporary. It works because of how Python schedules evaluation around the equals sign.
Python evaluates the right side completely before any assignment happens on the left. The right side last_item, first_item packs the current values into a temporary tuple ("HDMI Cable", "Wireless Mouse"). Only after that tuple exists does Python begin unpacking it into the names on the left.
Here is the order of operations as a diagram:
The diagram shows why a temporary variable isn't needed: the temporary tuple is the temporary. Once both old values are safely captured in that tuple, assignment to a and b can happen in any order without one clobbering the other.
The same idea scales beyond two variables. You can rotate three or more values in one line:
The right side packs (second, third, top) with the old values, then the left side unpacks into the same three names in the new order. The right side completing before any binding happens is what makes rotations safe.
Basic unpacking requires the number of names to match exactly. The starred target *name, introduced by PEP 3132 in Python 3.0, relaxes that. A starred name captures any number of elements, including zero, into a list. You can place the star at the start, middle, or end of the pattern.
first takes the first element. last takes the last element. *middle collects everything in between as a list. The fixed names at the edges are satisfied first, then the starred name absorbs whatever remains.
A diagram makes the partitioning clearer:
The two fixed names eat one element each from the edges, and the starred name takes whatever is left in the middle. The starred slot can hold zero, one, or many elements.
Here is the surprise that catches people: the starred target produces a list, even though we unpacked from a tuple.
This isn't a typo in Python; it's a deliberate design choice. A starred target always collects into a list, regardless of the source type. The reasoning is that the captured chunk is usually inspected, iterated, or mutated by the caller, and a list is the natural type for that. If the source is a tuple and you want the chunk to be a tuple too, wrap it explicitly: middle = tuple(middle).
The starred target also accepts an empty capture. If the fixed names eat every available element, the star ends up with an empty list, not an error:
first and last consume both elements, and middle ends up as []. The unpacking only fails when the fixed names can't be satisfied, like trying first, *middle, last = ("X",), which raises ValueError: not enough values to unpack (expected at least 2, got 1).
Cost: A starred target allocates a new list and copies references into it. For first, *rest = huge_tuple with a million elements, rest allocates space for 999,999 references. If you only need to iterate the tail, slicing or itertools.islice may be cheaper.
Only one starred name is allowed per pattern. Two would make the split ambiguous, so Python rejects it at parse time:
This is a SyntaxError, not a ValueError. Python refuses to compile the code because the meaning isn't defined.
_When you only care about some of the values in a tuple, the Python convention is to use _ (a single underscore) for the names you want to ignore. The underscore is a regular variable; the convention is purely social.
Two _ slots are bound to "Wireless Mouse" and 120, but the name signals "I don't intend to use these". Linters and reviewers recognize the convention and won't flag unused variables named _.
A small detail: _ is a normal name, so its value persists. After the unpacking above, _ holds 120, the last value bound to it. If you ran print(_), you'd see 120. The convention says "don't read this", not "Python forbids it".
You can combine _ with a starred target to throw away the middle and keep only the edges. The idiom first, *_, last reads as "give me the first and last, discard everything in between":
The middle three elements end up in _ (as a list), and the intent is clear at a glance. This is much cleaner than shipment[0] and shipment[-1] on two separate lines, especially inside a for loop where the same pattern would otherwise need two index lookups per pass.
Python doesn't actually have "multiple return values". When a function appears to return more than one value, it's returning a single tuple, and the caller unpacks it. This is one of the most common reasons tuples show up in real code.
The return total, item_count line packs the two values into a tuple. Without unpacking on the caller side, result is just that tuple. To work with the two values as separate names, unpack at the call site:
The call returns a two-tuple, and the unpacking on the left binds total and item_count in a single line. The function's interface reads as "returns total and item count", which is more honest than always returning a single value and forcing callers to index in.
When a function returns more than three or four values, that's a hint to switch to a namedtuple or a dataclass so the fields have real names.
for LoopsThe loop variable in a for statement is just a binding target, the same as the left side of an =. Anywhere binding happens, an unpacking pattern works, and a for loop over a sequence of tuples is the most common place you'll use unpacking outside an assignment.
Each item in cart is a three-element tuple. The pattern name, qty, unit_price in the for header binds three names per pass. The loop body works with the names directly, with no item[0] or item[2] cluttering the math.
The same starred-target rules apply inside the loop header. If each tuple has a fixed prefix and a variable tail, capture the tail with a star:
order_id takes the first element of each tuple, and *items captures the rest as a list. Each pass sees a different-sized items, and the unpacking handles every shape without modification to the loop.
enumerateThe enumerate built-in yields (index, value) tuples while iterating. Unpacking the pair in the for header gives you both names in one step, which beats tracking a manual counter.
enumerate(cart) produces (0, "Wireless Mouse"), then (1, "USB Cable"), and so on. The pattern index, item in the loop header unpacks each pair as it arrives. Without unpacking, you'd write pair = ... and then pair[0], pair[1] inside the body, which is exactly the kind of indexing that unpacking eliminates.
You can start counting at a different value with enumerate(cart, start=1). The unpacking pattern doesn't change; only the values produced by enumerate do.
zipThe zip built-in pairs elements from two or more iterables and yields a tuple per position. Like enumerate, it's a natural fit for unpacking in the loop header.
zip(names, prices) produces ("Wireless Mouse", 29.99), then ("USB Cable", 4.99), then ("HDMI Cable", 14.99). The pattern product, price unpacks each pair. The loop ends when the shortest of the zipped iterables is exhausted, so mismatched lengths silently truncate; pass strict=True (Python 3.10+) to raise an error on mismatch.
zip works with three or more iterables, and the pattern just gets one more name. Three iterables produce three-element tuples per pass and need three names in the unpacking pattern. The shape of the data and the shape of the pattern always match.
When a tuple contains other tuples, the unpacking pattern can mirror that shape. Group the inner names with parentheses (or square brackets; both work) to nest one level deeper.
The outer tuple has two elements: a string and an inner tuple. The pattern name, (lat, lon) says "the first element binds to name, and the second element is itself a two-element sequence I want to unpack into lat and lon". The shape of the pattern has to match the shape of the data, or ValueError follows.
Nested unpacking shines in for loops over tuples-of-tuples:
Each item is a two-element tuple whose second element is also a two-element tuple. The pattern order_id, (lat, lon) binds three names per pass. Without nesting, the loop body would need coords = item[1]; lat = coords[0]; lon = coords[1], which is three lines of bookkeeping for what one line of unpacking does cleanly.
Nesting can go deeper, but readability degrades fast. Two levels are usually fine. Three or more is a signal to flatten the data, switch to a namedtuple, or unpack one level at a time inside the loop body.
A few packing and unpacking mistakes show up repeatedly in real code. Each one has a clean fix.
Pitfall 1: Mismatched arity. Forgetting that basic unpacking is strict produces ValueError at runtime. The fix is to count both sides, or use a starred target when the source size varies.
The source has four elements, the pattern has three names. Add a fourth name, discard the extra with _, or use * to absorb the tail:
The starred *_ captures everything beyond the third element, so the unpacking succeeds whether the tuple has four elements or fourteen.
Pitfall 2: Unpacking a string by accident. Strings are iterable. Unpacking one binds each character to a name, which almost never matches intent.
If you expected code to be a three-tuple and it turned out to be the string "ABC", the unpacking silently splits the characters. The bug only surfaces when the values are used downstream and have the wrong type. Defensive fix: check isinstance(code, tuple) before unpacking, or pack the string into a one-tuple deliberately with (code,) if that's what you want.
Pitfall 3: Forgetting the comma in a singleton. Parentheses without a trailing comma are just grouping. To pack a single value into a tuple, you need the comma.
This catches people when they write code like def get_items(): return ("Wireless Mouse") expecting a one-tuple, and the caller's name, = get_items() then raises TypeError: cannot unpack non-iterable str object ... actually it would unpack the string into characters because strings are iterable, and the bug would only manifest as a wrong-shape result downstream. Either way, the fix is the same: add the trailing comma whenever you mean a one-element tuple.
10 quizzes