There are 2 types of people. Those who do backups and those who will do backups.
Hey pips!
Let’s say we’ve defined a function that takes in 3 parameters:
def create_student(name, age, house = None):
return {
"name": str(name).capitalize(),
"age": round(age),
"house": house or "unassigned",
}
When we call this, we pass in the arguments individually:
>>> create_student("Stralian", 21, "Haven")
{"name": "Stralian", "age": 21, "house": "Haven"}
But suppose we had our items stored in a list
. How could we pass them to each parameter?
>>> info = ["Stralian", 21, "Haven"]
If we just pass info
to the function, it assigns the whole list to the first parameter name
, leaving age
and house
without values:
>>> create_student(info)
TypeError: create_student() missing 1 required positional argument: 'age'
What we want is for the items of the list to ‘spread’ out onto each parameter. We can do this manually by indexing the list:
>>> create_student(info[0], info[1], info[2])
{"name": "Stralian", "age": 21, "house": "Haven"}
But sheesh, that’s not ergonomic at all. If we had 5 variables, we’d have to write out all the indices up to [4]
.
This is another situation where the snowflake operator *
comes in handy. When used to define variables (function definitions, variable unpacking), it packs many arguments into one – but here it does the opposite, unpacking the values of the list:
>>> create_student(*info)
{"name": "Stralian", "age": 21, "house": "Haven"}
To visualise what’s going on here:
So the *
operator used before an iterable unpacks it, turning it from 1 object into a “series” of individual arguments that can be mapped to the parameters of a function. You can also think of it like a magic operator that ‘slices off’ the brackets around an iterable.
This is very much syntactic sugar! You can only use *
like this between the ()
of a function call. Trying to apply it to an object raw doesn’t work:
>>> stuff = [1, 2, 3]
>>> *stuff
SyntaxError: can't use starred expression here
Let’s look at another scenario which illustrates the difference between passing in an iterable argument vs unpacking it with *
. Our good friend print()
joins us again. If we print a list
, it outputs the string representation of the list object:
>>> naturals = [0, 1, 2, 3, 4]
>>> print(naturals)
[0, 1, 2, 3, 4]
But if we unpack, then each item of the list is printed individually, with the default space separator:
>>> print(*naturals)
0 1 2 3 4
The difference is even clearer if we add a separator:
>>> print(*naturals, sep = " / ")
0 / 1 / 2 / 3 / 4
# no snowflake, no sep
>>> print(naturals, sep = " / ")
[0, 1, 2, 3, 4]
>>> print(naturals, "hi", sep = " / ")
[0, 1, 2, 3, 4] hi
Another place we can use this is when constructing a new iterable. If you’ve ever needed to merge lists, you can do so by unpacking them into a new one:
>>> list1 = [1, 2, 3]
>>> list2 = [4, 5, 6, 7]
>>> [*list1, *list2]
[1, 2, 3, 4, 5, 6, 7]
You could of course just concatenate them with +
, but if you also have other items, this can make things a lot clearer:
>>> ["first", *list1, *list2, [0, 1]]
["first", 1, 2, 3, 4, 5, 6, 7, None]
>>> ["first"] + list1 + list2 + [[0, 1]]
["first", 1, 2, 3, 4, 5, 6, 7, None]
Like many of Python’s quirkier features, the snowflake operator gives us so much flexibility and convenience. It’s something you really come to miss in other languages![^miss]
[^miss]: Thank goodness JavaScript has the spread ...
operator.