Keyword-only arguments

While the for ... else ... form of for loops is rather a curiosity that not many developers are eager to use, there is at least one lesser-known feature in Python syntax that should be used more often by every Python programmer. This feature is keyword-only arguments.

Keyword-only arguments is a feature that has been in Python for a very long time, but initially was only found in some built-in functions or extensions that were built with the use of the Python/C API. But, starting from Python 3.0, keyword-only arguments are an official element of language syntax that can be used in any function signature. In function signatures, every keyword argument defined after  a single literal * argument will be marked as keyword-only. Being keyword-only means that you cannot pass a value as an positional argument.

In order to better understand what problem is being solved by keyword-only arguments, let's consider the following set of function stubs that have been defined without that feature:

def process_order(order, client, suppress_notifications=False):
...


def open_order(order, client):
...


def archive_order(order, client):
...

The preceding API is pretty consistent. We can clearly see that every function takes exactly two of the same arguments that are probably crucial for every part of the program that needs to deal with orders. We can also see that the additional suppress_notifications argument in the process_order() function stands out. It has a default value, so it is probably a flag that can be switched on and off. We don't know what this program does, but from the API, we can guess how these functions could be used. The most simple example could be as follows:

order = ...
client = ...

open_order(order, client)
process_order(order, client)
archive_order(order, client)

Everything seems clear and simple. However, a curious API designer would see that there is something disturbing in the API design that can become a problem in the future. If there is a need to suppress notifications in the process_order() function, the API user can do this in two ways:

process_order(order, client, suppress_notifications=True)
process_order(order, client, True)

The first usage is best, as it makes the semantics of the function call clear. Here, the two leftmost arguments (order and client) are best when presented as positional arguments, because they have dedicated meaningful variable names, and it also seems that their position is conventional to the API. The meaning of the suppress_notifications argument will be totally lost if we present it as a plain literal True value.

What is more worrisome is that such lax constraints on API usage puts the API designer in a rather uncomfortable position where he/she must be extremely cautious when extending the existing interfaces. Let's imagine that there is new requirement to suppress payment on demand; we should be able to do this by adding a new argument named suppress_payment. Signature change is rather simple:

def process_order(
order, client,
suppress_notifications=False,
suppress_payment=False,
):
...

For us, the intended usage is clear – both suppress_notifications and suppress_payment should be provided to the function as keyword arguments and not positional arguments. But, what is clear to us doesn't have to be clear to our users. It is just a matter of time until we start seeing function calls like the following:

process_order(order, client, True)
process_order(order, client, False)
process_order(order, client, False, False)
process_order(order, client, True, False)
process_order(order, client, False, True)
process_order(order, client, True, True)

This pattern is dangerous for yet another reason. Imagine that someone less familiar with the general design of the API added a new argument, not at the end of the argument list but just before other arguments that were supposed to be used as keywords. Such a mistake would invalidate all existing function calls where keywords arguments were wrongly passed positionally.

In large projects, it is extremely hard to protect your code from such misuse. And, without enough protection, every misused call to your functions will, over the years, create a large amount of debt that can greatly reduce your effectiveness. The best way to protect your function signatures from this kind of erosion is by explicitly stating which arguments should be used as keywords. In the discussed example, this approach would look as follows:

def process_order(
order, client,
*,
suppress_notifications=False,
suppress_payment=False,
):
...