tuple assignment
Tuple assignment is Python's a, b = b, a + b: build a tuple on the right, then unpack it into
names on the left. The solutions below all use it for the same job. Each one carries a pair of
numbers forward through a recurrence, and each new pair is computed from both members of the old
one. Done as two ordinary assignments, the first would overwrite a value the second still needs.
Tuple assignment avoids that, because the whole right-hand side is evaluated before any name is
rebound. This page is part of the language-idioms series, so it covers the idiom, why these four
solutions use it, and what the C versions write instead. The neighbouring
parallel assignment page is about the general concept of
updating several variables at once, in any language. This page is about the Python mechanism
behind it: packing, unpacking, and the swap.
The idea
The language reference defines the order exactly. An assignment statement first evaluates the expression list on the right. With a comma in it, that produces a single tuple. Then it assigns the tuple's items to the targets on the left, one at a time, from left to right. So in
x, y = (3 * x + 4 * y, 2 * x + 3 * y)
both products are computed from the old x and y and stored in a temporary tuple, and only
then are x and y rebound. The temporary exists, but you never name it. (The parentheses are
house style. The comma makes the tuple, not the brackets.) The swap a, b = b, a is the smallest
case: the right side holds the two old values, and the left side writes them back in the other
order.
Each of the four problems uses the idiom with a different recurrence:
| Problem | Pair carried | Update |
|---|---|---|
| 2 | two consecutive even Fibonacci terms | (a, b) → (b, 4b + a) |
| 57 | numerator and denominator of the √2 convergent | (p, q) → (p + 2q, p + q) |
| 100 | a solution of the Pell equation x² − 2y² = −1 | (x, y) → (3x + 4y, 2x + 3y) |
| 169 | two neighbouring values of Stern's diatomic sequence, walked one bit of n at a time | one member replaced by the sum, the other kept |
Problem 2 is a shift: the old b becomes the new a, and only the new b is real arithmetic.
Problems 57 and 100 are full 2×2 linear maps, so each new value reads both old ones. These are
the cases where two plain statements give the wrong answer. Problem 169 is the odd one out, and
the C section explains why it matters.
There is a second reason to write the update this way, besides correctness. Each tuple assignment
is one row-pair of a linear recurrence.
x, y = 3*x + 4*y, 2*x + 3*y is the matrix [[3, 4], [2, 3]] applied to the column (x, y), written
out on one line. A reader can check it against the derivation in the notes without tracking which
variable has already been overwritten. Once the update is visibly a matrix, it is also obvious how
to jump k steps at once with matrix exponentiation
when the step count is too large to walk.
Unpacking is the same mechanism
The left side of the statement is a general target list, so the same rule handles more than
swaps. Problem 57 initialises three names in one line (numerator, denominator, result = (1, 1, 0)).
Problem 2 starts its generator with even_fib_a, even_fib_b = (2, 8).
The same unpacking appears in for k, v in d.items(), in q, r = divmod(n, d), and with a starred
target in first, *rest = seq (PEP 3132). In every case the
right side is fully evaluated first and then split. If the number of items doesn't match the
number of targets, Python raises ValueError at that point. It never truncates or pads silently.
The counterpart in C
C has no tuple to pack into, so the temporary has to be written out by hand. Three of the four C versions do this, and two of them say why in a comment. Problem 100:
/* Use temporaries so the new x does not clobber the old x before y is computed. */
long long nx = 3 * x + 4 * y;
long long ny = 2 * x + 3 * y;
x = nx;
y = ny;
This is the tuple assignment with its hidden tuple given names (nx, ny). Problem 2 needs only
one temporary, because half of its update is a plain shift: compute next = 4 * b + a, then
a = b, then b = next. The order of those last two lines matters, and in Python the tuple form
removes that concern.
Problem 57 shows a cost the Python version hides. Its numbers are big integers, so the C version
keeps them in a fixed-size BigInt struct, computes new_num and new_den, and copies them back
with struct assignment (num = new_num). That is valid C, but each copy moves the whole limb
array, on every iteration. In Python the "copy" only rebinds a reference to an int object that
already exists, so the rebinding costs nothing. If the copy ever showed up in a profile, the C
version could swap pointers instead and alternate between two buffers. That is the
pointer-level version of a, b = b, a.
Problem 169 shows the reverse. Its Python update looks like a full tuple assignment: one branch of
the bit test writes a, b = a, a + b, and the other the mirror image. But in each branch, one
target just gets its own old value back. Only one variable actually changes, and the value it
needs is still unchanged. So the GMP version in C needs no temporary at
all. It does a single in-place mpz_add into whichever of a or b the bit selects. The tuple
form is convenient for keeping the two branches symmetric. In C, writing out the dependency shows
it was never a simultaneous update.
The classic C swap is the three-line tmp dance. The XOR swap (a ^= b; b ^= a; a ^= b;) is a
curiosity, not an optimisation: it is slower on modern hardware, and it zeroes the value if both
operands are the same object. A compiler turns the tmp version into register renaming anyway.
CPython does something similar: since 3.11 it compiles two- and three-name swaps to a stack
SWAP instruction, without building the tuple at all.
How to reason about it
- Use it whenever a new value reads an old value that is also being replaced. That is the test. If every right-hand side only reads variables that keep their value, sequential statements are fine, and in C they are cheaper.
- Read the update as a matrix. If the pair update is linear, writing it as one tuple assignment makes the matrix visible. That makes it easy to check against the derivation and easy to turn into a fast power later.
- Watch the target order when the targets depend on each other. Targets are assigned left to
right.
i, a[i] = j, vwritesa[j], nota[old i], becauseiis rebound beforea[i]is evaluated as a target. Keep subscripted targets independent of names that the same statement rebinds. - In C, name the temporaries after the values.
nx/nyornew_num/new_denturn the hidden tuple into readable code. A comment saying why (as in 57 and 100) prevents someone from later "simplifying" it into a bug. - Mind what one assignment copies. In Python, assignment only rebinds names. In C, assigning a struct copies all of it. For large values, swap pointers rather than contents.