..

Python3.8 Positional-only arguments

Introduction

I have always been of the mind that explicit is much better than implicit. For this reason, I have almost always included Python’s keyword-only arguments when defining a function.

My reasoning behind this is I feel that I should always know what I am passing into a function and how it’s intended to be used. Below is an example:


def divide(*, numerator: int, denominator: int) -> float:
    """
    Divide numerator by denominator, return float.
    """
    return numerator/denominator

In this example, you must define which parameter use named keyword arguments, eg divide(numerator=100, denominator=50). If you try calling the function without doing this eg divide(100,50), you will get the following error: TypeError: divide() takes 0 positional arguments but 2 were given. This forces the user to be explicit.

However, this is only a good strategy when your variable names carry meaning like in the division example. Not all variables do carry important information. However, we can see this in many built-in python functions for example, len. The name of the variable in len’s definition is obj. This makes perfect sense since we are trying to find the length of an object. Because this is implicit and well-known knowledge, it would be silly to force a user to pass in obj when calling len.

Now you could say, in the case of functions like len, let’s just not include the keyword-only argument. Doing so means that the user does not have to define it as a keyword, but a user still can. Giving a user the ability to do this can have some detrimental effects. It means that users can call the function in two different ways, either len(list_object) or len(obj=list_object). Why not be explicit in how you want your function to be called?

This is where Python3.8’s Positional-only argument comes in! As a part of PEP 570 the / argument. This argument will force all arguments preceding it, to be positional only, meaning that the user must provide them as positional arguments and not give them as keyword arguments.

Trivia on the / character

It is important to remember that it is all elements preceding the /

Interesting bit of trivia on the choice of the / character:

Alternative proposal: how about using ‘/’ ? It’s kind of the opposite of ‘*’ which means “keyword argument”, and ‘/’ is not a new character. Guido van Rossum in 2012

Example function using positional-only arguments

With this new tool in our tool belt, we can define functions like this:

def add_ints(a: int, b: int, /) -> int:
    """
    Add the two passed in ints and return the result.
    """
    return a+b

With the function defined above, you can only call it like this: add_ints(1,5) and not like this: add_ints(a=1, b=5). If you call it like the latter way, you will get the following error: TypeError: add_ints() got some positional-only arguments passed as keyword arguments: 'a, b'

Combining positional-only arguments with keyword-only arguments

You can also use this in conjunction with the keyword-only argument for even more explicit goodness. But doing so will complicate the API into the function; forcing the user to pass in different arguments in different ways would be very frustrating.

The power of the positional only shows itself when you are defining functions that are used regularly with a small argument list. We looked at the len example above, which is the perfect example of where the positional only argument should be used. The goal is to reduce the effort required to parse the code mentally. If you see "foo1bar".split(sep="1") that takes a little longer to read than "foo1bar".split("1"). This is just because, as a collective, we are all used to the arguments for these functions.

The use case for positional arguments only is relatively small, and I think it should be used very sparingly, as you run the risk of getting the function users to jump through hoops just to use your function.