Keyword-only Arguments in Python

The magic of asterisk in Python — Part II

Syahrul Hamdani
Analytics Vidhya

--

Photo by Cederic X on Unsplash

So, you’ve read the basic magic of asterisk in python and now want more? Sure. Time to enter the castle. If you’re not ready yet, feel free to visit that previous article, and come back when you feel you’re ready. Or at least, you’ve tried your own.

In this article, we will talk mostly about PEP3102 which proposed keyword-only arguments for python.

Keyword Arguments

In the python glossary, you’ll find a section defining what argument means in python. Just like any other programming language, an argument is a value passed into the function (or method) when doing function calls. In python, there are 2 kinds of argument:

  1. Keyword Argument
    A keyword argument is an argument preceded by an identifier (named arguments). Notice that the value here is passed in a function call and not the default value when defining function parameters.
  2. Positional Argument
    A positional argument, simply, is all argument that is not a keyword argument.

One example is when using requests.get, we often pass the URL in a string directly. In this scenario, we just give the function a positional argument for parameter url. But, if we pass a value something like requests.get(url='https://your-url.com/') , then we’re passing in a keyword argument.

Don’t make yourself confused with required and optional argument. These 2 are due to when we define a function with its parameter(s). If we include a parameter with default value, later the argument become optional. Otherwise, we are required to pass an argument, hence required positional argument.

Now, when we make a function call, it’s up to us to pass arguments as a positional or keyword argument. But, as python upholds readability, the consideration is if the arguments are obvious to be passed in, then we just pass the value as a positional argument. In contrast, also most likely, we want other users to know what value we pass into what parameter. So, keyword argument.

Keyword-only Arguments

Using asterisks when defining a function, gives us a higher level of magic. It’s called keyword-only arguments. This type of argument has a similar behavior as that vanilla keyword argument, but it forces users to pass keyword arguments rather than positional or direct arguments.

def generate_username(*users, separator, length=None):
users = [user.lower().split() for user in users]
username = [separator.join(user) for user in users]
if length:
username = [user[:length] for user in username]
return username

Let’s use the above function as an example. The function will generate usernames in a simple way, with users’ custom separator. Notice that it uses packing arguments there. So, we can call it like

>>> generate_username("John", "Mr Smith", "Mrs Smith", separator="_")

and will result ['john', 'mr_smith', 'mrs_smith'].

Now, where’s the magic you ask? Try passing "_" as a positional argument like below.

>>> generate_username("John", "Mr Smith", "Mrs Smith", "_")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: get_username() missing 1 required keyword-only argument: 'separator'

Take that error! You’re missing 1 required keyword-only argument: separator. What’s that? That, actually, is the effect of using an asterisk in a function followed by a required parameter, separator.

As PEP 3102 stated, every parameter defined after an asterisk in a function needs to be passed keyword-only arguments. That is, by specifying the parameter name. This allows us to define additional parameters after asterisks that won’t be packed together. Since we embed the asterisk with parameter users, it becomes an optional positional packing argument.

The optional positional packing argument is a packing argument just like described in the previous article.

The above function will suck up any positional arguments before we pass a keyword argument for separator. Now what if we don't want to capture any positional arguments, in the above example, for parameterusers. We can just use the magic, just a single asterisk. Let’s redefine the above function.

def generate_username(users, *, separator, length=None):
users = [user.lower().split() for user in users]
username = [separator.join(user) for user in users]
if length:
username = [user[:length] for user in username]
return username

The difference here is we can’t pass any number of positional arguments, instead we pass it into the parameter users as a collection (list or tuple).

>>> list_users = ["jhonny", "Jadon Smith", "Mr Smith", "Mrs Smith"]
>>> generate_username(list_users, separator="_")
['jhonny', 'jadon_smith', 'mr_smith', 'mrs_smith']

If we pass a "_" as a positional argument, it will throw an error like below.

>>> generate_username(list_users, "_")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: generate_username() takes 1 positional argument but 2 were given

This is because any leftover parameters after the asterisk * will require a keyword to pass the arguments, as stated in the PEP 3102 below.

Since Python requires that all arguments be bound to a value, and since the only way to bind a value to a keyword-only argument is via keyword, such arguments are therefore “required keyword” arguments.

Takeaways

Using the asterisks in python, we have 2 options to define a function with keyword-only arguments:

  1. Keyword-only arguments with a positional argument
  2. Keyword-only arguments without positional argument

These 2 scenarios are proposed first in the PEP 3102, you can read more about the rationale why this magic exists.

References

[1] Trey Hunner, Asterisks in Python: what they are and how to use them (2018)

[2] PEP 3102

--

--