skip to content

Unpacking and Starred Assignment

Python destructures sequences on the left of an assignment, captures a remainder with *rest, and splats iterables and mappings into calls. It appears in almost every idiomatic snippet.

part ofPythonoverview, primer and where to startread it →
on this pageshow

questions

4

What does `a, b = b, a` do in Python, and why is no temporary variable needed?

level: juniorimportance: must knowfreq 78%

answer

  1. Order of evaluation is the whole answer
  2. Something is built before anything is bound
  3. Both old values captured first
  4. Targets are bound left to right
  5. Mismatch raises, never truncates silently

basics

~20 s

Python evaluates the entire right-hand side first, building the pair (b, a), then binds the targets on the left one at a time. Both original values are captured before either name is rebound, so no manual temporary is needed.

solid answer

~50 s

An assignment in Python evaluates the whole right-hand expression before it binds anything on the left. `b, a` is a tuple built by the comma, so both current values are read into that pair first; only then does Python bind `a` to the first item and `b` to the second, left to right. That ordering is what makes the swap safe — nothing is clobbered because nothing has been rebound yet. It is not a special swap syntax: it is ordinary iterable unpacking, the same machinery behind `x, y = point` and `for key, value in mapping.items()`. If the counts disagree Python raises `ValueError`; if the right side is not iterable it raises `TypeError`. CPython optimises the two-name case into a stack swap, so no tuple is actually allocated, but the semantics are defined as if it were.

code

python · 3 lines
python
a, b = 1, 2
a, b = b, a
print(a, b)  # 2 1

go deeper

for a junior

Recall the two-phase rule and be able to say it in one sentence: the right side is evaluated first, then targets are bound left to right. Being able to write the swap is not enough — interviewers ask why it is safe.

for a middle

Explain the mechanics: the comma builds a pair, unpacking accepts any iterable, targets may be attributes or subscripts, and arity mismatch raises ValueError while a non-iterable raises TypeError. Mention that CPython optimises the small cases into a stack swap.

for a senior

Show where the ordering actually causes bugs — a target list that mixes a name with a subscript using that name — and say when you would refuse the idiom for readability, such as unpacking five positional fields instead of using named access.

for a principal

Own the style call: unpacking is a readability tool with a hard limit. Decide where your codebase draws the line between positional destructuring and named records, and note that a wrong-arity ValueError in production is a shape contract you failed to assert at the boundary.

### The rule behind the idiom Python's assignment statement runs in two phases, and the order is fixed by the language reference: the expression to the right of `=` is evaluated **completely first**, then the target list on the left is bound **left to right**. In `a, b = b, a` the right-hand side is a tuple built by the comma: Python reads the value currently bound to `b`, reads the value currently bound to `a`, packs them into a pair, and only afterwards rebinds `a` to that pair's first item and `b` to its second. Both original values were captured before either name changed — which is exactly the job the temporary does in the three-line `tmp = a; a = b; b = tmp` dance other languages need. That is a *semantic* description, not necessarily the machine code. CPython compiles the two- and three-name cases into stack manipulation rather than a real object: disassembling the statement with `dis.dis` on 3.14 shows two loads, a stack swap and two stores, with no tuple ever allocated. The observable behaviour is specified as if the tuple existed, so you can reason with the tuple model and still get the fast path. ### One mechanism, not a special form The swap is not a dedicated feature. It is ordinary unpacking assignment, and everything true of unpacking is true of it: - **Any iterable works on the right.** `a, b = [1, 2]`, `a, b = "hi"` and `a, b = range(2)` all bind two names. The right side does not have to be a tuple. - **Targets can be more than names.** An attribute, a subscript or a nested target list is assignable, so `obj.x, obj.y = obj.y, obj.x` and `lst[i], lst[j] = lst[j], lst[i]` are the same idiom. Nested targets mirror the shape of the data: `name, (host, port) = record` unpacks a pair inside a pair. - **`for` targets use the identical machinery.** `for key, value in mapping.items():` and `for index, item in enumerate(seq):` are unpacking assignments performed once per iteration. - **"Returning multiple values" is really returning one tuple** that the caller unpacks, which is why `low, high = bounds()` reads so naturally. ### Arity is checked, and loudly Unpacking never silently truncates or pads. Too few values on the right gives `ValueError: not enough values to unpack (expected 3, got 2)`; too many gives `ValueError: too many values to unpack (expected 2)`. A non-iterable right side is a different failure: `x, y = 5` raises `TypeError: cannot unpack non-iterable int object`. Knowing which exception you get tells you which mistake you made — a shape mismatch versus a type mistake — and that distinction is worth stating in an interview. Since 3.11, fine-grained error locations (PEP 657) underline the offending target in the traceback, which makes a wrong-arity line in a long expression much easier to find. ### Where left-to-right binding actually bites The two phases matter as soon as one target's meaning depends on another target. The canonical demonstration, straight from the language reference: with `x = [0, 1]` and `i = 0`, the statement `i, x[i] = 1, 2` leaves `x` as `[0, 2]`, not `[2, 1]`. The right side `(1, 2)` is built first; then `i` is bound to `1`; then `x[i]` is resolved — *with the new `i`* — and element 1 is set to 2. The same trap appears in the swap form when an index is itself being swapped, such as swapping a value with the element it points at: the second target sees the first target's new value. When a target list mixes a name and a subscript that uses that name, spell the assignment out on separate lines instead of trusting readers to recall the ordering. ### Small things interviewers probe A single-element target list needs the trailing comma — `only, = f()` unpacks a one-item result and raises `ValueError` if the result has any other length, which makes it a useful assertion. Chained assignment is a different animal: `a = b = expr` binds both names to the *same* object rather than distributing two values. And unpacking is fine for a handful of values but a poor way to take apart a long sequence; readers stop being able to count targets past three or four, and named access beats positional at that point. This behaviour is unchanged across every Python 3 release, 3.14 included; only the error messages and traceback precision have improved.

  • What exception does Python raise when the target count and the value count disagree in an unpacking assignment?
    `ValueError`, with a message that names both counts: `not enough values to unpack (expected 3, got 2)` or `too many values to unpack (expected 2)`. It is never a silent truncation. A non-iterable right-hand side is a different error — `TypeError: cannot unpack non-iterable int object` — so the exception type tells you whether you got the shape wrong or the type wrong.
  • With `x = [0, 1]` and `i = 0`, what does `i, x[i] = 1, 2` leave in x, and why?
    `[0, 2]`. The right-hand side `(1, 2)` is evaluated first, then targets are bound left to right: `i` becomes `1`, and only then is `x[i]` resolved — using the new `i` — so element 1 is set to 2. Element 0 is untouched. When a target list mixes a name and a subscript using that name, write the two assignments separately.
  • Does the swap idiom work when the targets are attributes or list elements rather than plain names?
    Yes. Any assignable target is allowed, so `obj.x, obj.y = obj.y, obj.x` and `lst[i], lst[j] = lst[j], lst[i]` both work, and for the same reason: the right side is fully evaluated before any store happens. The one caution is a target whose subscript depends on another target in the same statement, because the later target sees the earlier one's new value.

It is like taking both cards off the table into your hand before dealing them back down: once both are in hand, it no longer matters which square you put down first.

saying these in an interview costs you the question

  • Claims Python has a dedicated swap operator with special rules
  • Says a is rebound before b is read, so a value is lost
  • Thinks parentheses are required for the right-hand side to work
  • Believes extra values are silently dropped instead of raising
  • Says unpacking only works on tuples, not lists or other iterables
  • Confuses ValueError on arity with TypeError on a non-iterable

context

open as a page

What does `first, *rest = items` bind in Python, and where may the star appear?

level: middleimportance: must knowfreq 60%

basics

~20 s

first takes one value and rest takes every remaining value as a list — always a list, whatever the right-hand side was. At most one starred target is allowed, and it may sit anywhere in the target list.

open as a page

In Python, what do `*` and `**` do at a call site and inside list or dict literals?

level: middleimportance: should knowfreq 52%

basics

~20 s

At a call site * spreads an iterable into positional arguments and ** spreads a mapping into keyword arguments. Inside a literal they splice contents in: [*a, *b] concatenates, {*a, *b} unions, {**m1, **m2} merges with later keys winning.

open as a page

A triage bot peeks with `head, *rest = fetch_tickets()`, yet every ticket's side effect fires at once and again later — why?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Starred unpacking is eager: it drains the generator to count the leftovers, so every side effect inside it runs at that line. Calling the generator function again creates a brand-new generator, so the whole stream runs a second time.

open as a page