Introduction
In Python, the arguments provided in a function call are passed by assignment (i.e. by object reference). This means that whenever a function gets called, every argument will become a variable pointing to the value that was specified. Obviously, the scope of these variables will be limited to the function being called.
This means that both the function and the caller share the objects by reference, without name aliasing. Therefore, by changing an argument name within the function we should not expect that the caller’s name will get changed, too. However, whenever we mutable objects get changed within a function, we should expect that this is going to have an effect to the caller as well, given that they both share the same object references.
In today’s article we will discuss about the difference between positional and keyword arguments when it comes to writing (or calling) functions in Python. Additionally, we will go through a few examples in order to demonstrate how *args
and **kwargs
can be used in your code in order to make it more effective and Pythonic.
Subscribe to Data Pipeline, a newsletter dedicated to Data Engineering
Positional vs Keyword arguments in Python
Arguments to Python functions (or methods) can be passed by keyword name or position and the language has the ability to collect both positional and/or keyword arguments in a single call.
Positional arguments refer to the type of arguments that are being parsed based on their positional index that they were provided when the function or method of interest gets called. Therefore, the order that the positional arguments are provided matters because they are matched from left to right.
As an example, let’s consider the built-in Python function [round()](https://docs.python.org/3/library/functions.html#round)
that is used to round the input number to the specified _ precision after the decimal point. The definition of the function is round(numbe_r[, _ndigit_s]) which means that an input number
is mandatory, whereas the ndigits
argument is optional and corresponds to the desired precision.
Now let’s suppose that we have a float variable a = 10.2254
that we want to limit into two decimal point precision. We can call do so by calling the round()
function and provide both arguments as positional arguments.
>>> a = 10.2254
>>> round(a, 2)
10.23
Since we provided positional arguments (i.e. we haven’t specified the keyword argument for each of the values provided in the caller), the first positional argument will correspond to number
and the second one will correspond to the option ndigits
argument, according to the function definition.
On the other hand, keyword arguments are being passed to a function by specifying the keyword in the form of name=value
. Therefore, the order these arguments are being passed in the caller doesn’t matter since they are matched by argument name.
Going back to the example with the round()
function, we could instead call it by passing keyword arguments.
>>> a = 10.2254
>>> round(number=a, ndigits=2)
10.23
And as mentioned already, the order that we provide keyword arguments doesn’t really matter:
>>> a = 10.2254
>>> round(ndigits=2, number=a)
10.23
Note that we could even combine both positional and keyword arguments where the latter should be specified after the former –
>>> a = 10.2254
>>> round(a, ndigits=2)
10.23
Note however that if you provide a keyword argument before positional arguments, a SyntaxError
will be raised.
SyntaxError: positional argument follows keyword argument
Positional arguments and *args
Now let’s suppose that we want to write a function in Python that accepts an arbitrary number of arguments. One option would be to pass the arguments in a collection – say a list – but this won’t be convenient in most of the cases. Additionally, this idea isn’t quite Pythonic – but this is exactly where arbitrary arguments list comes into play.
We could instead take advantage of the *args
idiom when defining our function in order to specify that it can actually accept any number of arguments. And it’s actually up to the implementation to properly deal with the provided arguments.
The star *
is called the unpacking operator and will return a tuple containing all the arguments provided by the caller.
As an example, let’s consider a fairly simple function that accepts an arbitrary number of integers and returns their sum
def sum_nums(*args):
sum = 0
for n in args:
sum += n
return sum
Now we can call the above function with literally any number of arguments we may wish:
>>> sum_nums(10, 20)
30
>>> sum_nums(10, 20, 30)
60
>>> sum_nums(5)
5
Note that you can combine normal and arbitrary arguments in the function definition:
def my_func(param, *args):
...
Keyword arguments and **kwargs
In the same way, we may sometimes want to write functions that could possibly accept an arbitrary number of keyword arguments.
When unpacking the keyword arguments from **
, the result will be a Python dictionary where keys correspond to keyword names and values to the actual argument values provided.
def my_func(**kwargs):
for key, val in kwargs.items():
print(key, val)
And now we can call the function with as many keyword arguments as we want to:
>>> my_func(a='hello', b=10)
a hello
b 10
>>> my_func(param1=True, param2=10.5)
param1 True
param2 10.5
Once again, it is useful to mention that arbitrary keyword arguments idiom can be combined with both normal arguments and arbitrary positional arguments:
def my_func(param, *args, **kwargs):
...
When to use *args and **kwargs
A very good practical example in which *args
and/or **kwargs
are usually useful is the decorator. In Python, a decorator is a function that accepts as argument another function, decorates it (i.e. it enriches its functionality) and it finally returns it.
Let’s assume that we want to create a decorator that will be in charge of reporting the execution time of a function to the standard output.
import functools
import time
def execution_time(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start_time = time.time()
func(*args, **kwargs)
end_time = time.time()
print(f'{func.__name__} took {end - start}s to run.')
return wrapper
The wrapper function will simply accept arbitrary positional and keyword arguments that will then be passed in to the function that gets decorated.
Final Thoughts
In today’s article we discussed about positional and keyword arguments and what their main differences are when it comes to writing or calling functions in Python.
Additionally, we discussed about the main difference between the *args
and **kwargs
and how you can potentially utilize them in your own functions based on what one may want to achieve. Furthermore, we showcased how you can consume positional and keyword arguments from *args
and **kwargs
in practise.
Finally, we went through a practical example to showcase an application of *args
and **kwargs
in the context of function wrappers (i.e. decorators).
It is important to mention that the actual notation makes use of star symbols (*
and **
) and both correspond to arbitrary argument lists. The names *args
and **kwargs
are nothing more than a convention* (which is quite popular in the community and usually used to describe arbitrary argument lists). Therefore, you do not have to reference them as such – for instance you could even name them as `helloand
hey` in your function definition even though I wouldn’t advise you to use such naming convention :).
Subscribe to Data Pipeline, a newsletter dedicated to Data Engineering
You may also like