Code is like humour. When you have to explain it, it’s bad.
Hey pips!
Right, remember how this is a thing?
x, y = y, x
In this issue we’re going to look at exactly why this works.
Hopefully you’ve already had some thoughts about what’s going on.[^thoughts] If I just add in some symbols though, it’ll probably immediately become obvious what’s going on:
[^thoughts]: Who doesn’t love a bit of thinking and problem-solving??
x, y = (y, x)
When you write y, x
, Python interprets this as a tuple. So in the statement, it first packages y
and x
into a tuple (y, x)
. This tuple doesn’t change, even when x
and y
are reassigned.
[!Warning] The reasons for this are a little involved. We might cover them some other time.
That’s step 1. But what’s happening on the left then?
Let’s change the right to be something else, and see what happens.
>>> x, y = "hi"
>>> x
'h'
>>> y
'i'
Ooh, interesting. Let’s try another iterable:
>>> x, y = [range(10), range(20)]
>>> x
range(0, 10)
>>> y
range(0, 20)
It looks like Python is ‘splitting’ the iterable between the 2 variables.
This kind of assignment is called a destructuring assignment, or more specifically to Python, variable unpacking. It lets us ‘break down’ and assign the individual items in an iterable to distinct variables. Isn’t that wild?
>>> first, second, third = [1, 2, 3]
>>> first
1
>>> second
2
>>> third
3
You’ll find _
commonly used as a throwaway variable:
wanted, _, important = get_some_output()
This makes no difference functionality-wise, but indicates to someone reading the code that you don’t intend to use the value assigned to _
.
So remember enumerate()
? When we use i, item
, Python is actually unpacking the output of enumerate()
behind the scenes:
for i, item in enumerate(["for", "so", "long"]):
...
# equivalent to:
for each in enumerate(["for", "so", "long"]):
i, item = each
# or more verbosely,
i = each[0]
item = each[1]
Of course, we do need the number of variables to match the number of items in the iterable…
p, q, r = [1, 2, 3, 4, 5] # what should happen here?
a, b, c, d, e = "rofl" # or here??
L, R = range(200) # ???
So we only usually use variable unpacking for iterables with a known and fixed shape (length). That’s almost always just to unpack pairs.
You can, however, ‘spread’ or ‘package’ surplus items into a single variable by prefixing 1 of the variables with *
. This causes it to become a ‘wild card’ which all of the extraneous items are assigned to.
from random import randint
l = [randint(-10, 10) for i in range(100)]
# capture first and last elements
first, *extra, last = l
# capture first 3 elements
first, second, third, *rest = l
We’ll see this *
snowflake fulfil a very similar function later when we look at function arguments. Stay tuned!
Further Reading
- Yeah, the syntax can get a little messy sometimes… StackOverflow↗