The splat operator, or *args and **kwargs in Python
Somebody once told me, the world is gonna roll me...
Summary
The *
operator has many usages in Python. Multiplication and power are the most common, but today we are going to see how it's used as the "splat" operator. If you come from JS, you know this as spread
. It's those mysterious *args
and **kwargs
you see once in a while, and they let you do:
Unpacking function arguments
>>> def describe_pet(animal, name, age):
... print(f"I have a {animal}, named {name}. It is {age} years old.")
...
... pet_details = ["dog", "WoofBox", 5]
...
... describe_pet(*pet_details)
I have a dog, named WoofBox. It is 5 years old.
Defining infinite (variadic) parameters
>>> def print_names_with_details(*args, **kwargs):
... for name in args:
... print(f"Name: {name}")
... for key, value in kwargs.items():
... print(f" {key}: {value.get(name, 'Not specified')}")
... print()
...
... print_names_with_details("Alice", "Bob", age={"Alice": 30, "Bob": 25}, location={"Alice": "Paris", "Bob": "Los Angeles"})
Name: Alice
age: 30
location: Paris
Name: Bob
age: 25
location: Los Angeles
Arguments vs parameters
A lot of devs use the name "arguments" and "parameters" interchangeably. It's because most of the time, it's good enough. The nuance between the two doesn't matter that much. I'm guilty of this myself, even in this blog. I loosely use one word for the other, because both are about a function input.
However, in today's article, the difference matters, because we are going to look at several use cases for *
in Python. Some are for unpacking arguments. Some are to create variadic parameters.
So for the purpose of clarity, I'll be more accurate this time.
Parameters are the variables you define in a function signature:
>>> def calculate_final_price(price, quantity, tax):
... return price * quantity * tax
Here, price
, quantity
and tax
are parameters.
However, arguments are what you pass to those parameters when you call the function:
calculate_final_price(30, 2, 0.75)
Here, 30
, 2
and 0.75
are the arguments.
Again, it's not that important most of the time. You can say "I pass 30 as a parameter", and everybody understands.
It's to make the post clearer.
Positional and keyword arguments
In Python, every function can have arguments passed using their position, or their name. Take this function:
def format_name(name, suffixes=(), prefixes=()):
suffixes = (" " + " ".join(suffixes)) if suffixes else ""
prefixes = (" ".join(prefixes) + " ") if prefixes else ""
return prefixes + name.title() + suffixes
We can call it by passing values using their position to know where goes what:
>>> format_name("kim rox", ["MD", "PhD"])
'Kim Rox MD PhD'
Those are then called "positional arguments".
Or we can use the name of the parameters to match the arguments, making the position irrelevant:
>>> format_name(suffixes=["MD", "PhD"], name="kim rox")
'Kim Rox MD PhD'
In this case, those are called "keyword arguments".
You can also mix and match:
>>> format_name("kim rox", prefixes=["Sir"], suffixes=["MD", "PhD"])
'Sir Kim Rox MD PhD'
Unpacking arguments
We talked about unpacking before, so you might want to read this article if it doesn't ring a bell.
But in short, it's the feature that lets you get all values from an iterable into individual variables, like this:
>>> first, second, third = ["gold", "silver", "bronze"]
>>> first
'gold'
>>> second
'silver'
>>> third
'bronze'
It turns out it also works with arguments, but you need to let Python know this is what you want, by using a *
:
>>> def calculate_final_price(price, quantity, tax):
... return price * quantity * tax
>>>
>>> bill = (30, 2, 0.25)
>>> calculate_final_price(bill) # no star, doesn't work
TypeError: calculate_final_price() missing 2 required positional arguments: 'quantity' and 'tax'
>>> calculate_final_price(*bill) # unpacking
15.0
The star forces each element of bill
to be unpacked into the function call, so that price
, quantity
and tax
all contain one of the values.
This is a shortcut, the manual equivalent would be:
>>> calculate_final_price(bill[0], bill[1], bill[2]) # unpacking
15.0
As you can see, the order of the elements in the iterable (here the tuple bill
) matters since unpacking will pass the arguments by position.
It's dependent on having the same number of parameters and values for the arguments. If we add one element in bill
, it fails:
>>> bill = (30, 2, 0.25, 0)
>>> calculate_final_price(*bill)
TypeError: calculate_final_price() takes 3 positional arguments but 4 were given
However, if one value is missing, you can pass it manually:
>>> bill = (30, 2)
>>> calculate_final_price(*bill, 0.25)
15.0
This cute little feature can also be used with mappings, such as dictionaries. This time, arguments will be passed by name, not by position, using the mapping keys. You need to use **
instead of *
:
>>> bill = {"tax": 0.25, "quantity": 2, "price": 30}
>>> calculate_final_price(**bill)
15.0
Be careful with the argument names, they must match the mapping keys exactly. They are case sensitive, but the order doesn't matter since we are now using keyword arguments.
One big trap beginners fall into is using *
(one star) by mistake with a dictionary.
Indeed, *
works on dictionaries!
It just doesn't do at all what you want: it will pass the keys as positional arguments.
So be careful to properly use **
(two stars) with dicts, unless you know what you are doing.
Variadic parameters
Have you noticed that some functions in Python can accept a seemingly infinite number of arguments?
Take print()
, it doesn't scream at you if you pass one, two or even ten of them:
>>> print('banana')
banana
>>> print('banana', 'apple', 'kiwi')
banana apple kiwi
>>> print('banana', 'apple', 'kiwi', 'grape', 'orange', 'pear')
banana apple kiwi grape orange pear
You can create such a function too.
But here is the trick: the syntax also uses *
, and it looks a LOT like the one we just saw in the previous section.
So many people confuse them all the time, despite the fact they do totally different things.
In the previous section, we saw *
applied on arguments to unpack them. This is done when we call the function.
Now, we are going to see how to use *
when we define the function parameters:
def a_function_with_infinite_params(*infinite_params):
print(type(infinite_params))
print(infinite_params)
Note the *
in the function signature, just before the parameter.
This tells Python to collect all values passed as positional argument, and group them in a tuple:
>>> a_function_with_infinite_params("banana")
<class 'tuple'>
('banana',)
>>> a_function_with_infinite_params("banana", "apple", "kiwi")
<class 'tuple'>
('banana', 'apple', 'kiwi')
We can have some fixed params, then collect what remains:
>>> def division(a, b, *numbers): # star here
... result = a / b
... for number in numbers:
... result = result / number
... return result
>>> division(8, 2) # no star here, that would be unpacking!
4.0
>>> division(8, 2, 2)
2.0
>>> division(8, 2, 2, 2)
1.0
>>> division(8, 1, 3)
2.6666666666666665
The first two values are assigned to a
and b
by position. Anything more is added to a tuple in the variable numbers
.
It is preferred to use a good name for your parameter with *
. If you really don't have a good one, then the most common name is args
.
Hence, sometimes you see:
>>> def division(a, b, *args): # star here
... result = a / b
... for number in args:
... result = result / number
... return result
There is nothing special about the name args
, it's the *
that matters. Don't be lazy, try to find a good name before falling back to using "args", which is not informative.
One of the rare good use cases for args
is for argument proxying, like in decorators. But most of the time, use a better name.
This way of declaring your function input bears the fancy name of "variadic parameters".
And as I assume you expect now, it also works with keyword arguments, but using **
:
>>> def a_function_with_infinite_params(**infinite_params): # two stars
... print(type(infinite_params))
... print(infinite_params)
>>> a_function_with_infinite_params(first="banana")
<class 'dict'>
{'first': 'banana'}
>>> a_function_with_infinite_params(first="banana", second="kiwi", last="pear")
<class 'dict'>
{'first': 'banana', 'second': 'kiwi', 'last': 'pear'}
You have to pass the arguments using keywords for it to work, and all the additional values will be grouped in a dictionary.
And yes, you can mix variadic with non-variadic ones:
>>> def log_attack(level, **bonuses):
... damages = level + sum(bonuses.values())
... print('- Base attack:', level)
... for name, value in bonuses.items():
... print(f"- {name.replace('_', ' ').title()}:", value)
... print(f'(Total hit points: {damages})')
...
>>> log_attack(5, rapier_of_unfairness=3, eleven_foot_pole=1)
- Base attack: 5
- Rapier Of Unfairness: 3
- Eleven Foot Pole: 1
(Total hit points: 9)
Finally, I encourage you to use good names for this as well. But the standard name for when you don't know what to use is kwargs
, for "keyword arguments".
You now know why you see *args, **kwargs
sometimes.
Adding to the confusion
OK, if you have been following so far, I'll need a few more neurons from you.
There is an additional syntax using *
in the parameter definition that does none of the above.
Yes, I know. Sorry.
It's the lone *
:
def a_functon_with_a_lone_star(param1, *, param2): # all alone :(
print(param1, param2)
It's used in Python to say "everything after the star MUST be passed as keyword argument". In our example, you can't pass param2
with position only:
>>> a_functon_with_a_lone_star("first", "second")
TypeError: a_functon_with_a_lone_star() takes 1 positional argument but 2 were given
*
requires that any argument after it must be passed using the keyword syntax, so we must do:
>>> a_functon_with_a_lone_star("first", param2="second")
first second
The most common use case for it is to force boolean flags to always be explicit. Take this code:
search('/home/', '.bashrc', False)
What does the False
do?
No idea.
But if you write:
search('/home/', '.bashrc', recursive=False)
Now it's much clearer.
So we can force the user to do the right thing by writing the function that way:
def search(directory, filename, *, recursive):
And what's the opposite? Well, forcing parameters to be positional!
This is done with /
:
def tetration(a, /, n):
result = 1
for _ in range(n):
result = a ** result
return result
This means I can write:
>>> tetration(2, 4)
65536
But not:
>>> tetration(a=2, n=4)
TypeError: tetration() got some positional-only arguments passed as keyword arguments: 'a'
This is used when position makes more sense than naming, such as some formulas. Although in the case of tetration
, you could argue if it's a good choice or not, since the exponent is on the left side in the IRL notation :)
In short:
function(*values, **more_values)
is for unpacking argumentsdef function(*params, **more_params)
is for infinite parametersdef function(param1, /, *, param2)
is for forcing positional or keyword argument syntax
All stars!
Just for fun.
>>> def multiply(*args, **kwargs):
... res = 1
... for i in (*args, *kwargs.values()):
... res = res * i
... return res
...
>>> multiply(*([2]*5), **{"*":2}) == 2**6
….
….
….
Hey, I had to try.
Just double checking my understanding here, but does using / to force positional parameters only apply to before the slash? As in it's the opposite of * in every way (positional vs keyword, before vs after)? You don't say explicitly one way or the other, but the TypeError mentions a but not n. So I'm not sure if it was a case of throwing its hands up after one error, or that's just all / was directing it to look for.
At first glance, I would have expected/ to apply to arguments after it, but I supposed that doesn't actually make sense, because my understanding is that you can't put positional arguments after keyword rguments.
And if my understanding of all this is correct, can you stack the slash and splat? Something like def do_math(num1, num2 /* operator): to force passing 2 numbers positionally and then the operator as a keyword argument?
I suppose you could get really fancy and use *args to perform the mathematical operation on any number of numbers like def do_math(*num) /* operator): But I suppose that makes the slash redundant, doesn't it?