Cogwheels (steampunk style) to represent Python functions

Using Positional-Only And Keyword-Only Arguments in Python [Intermediate Python Functions Series #5]

In previous articles in this series, you read about positional and keyword arguments, making arguments optional by adding a default value, and including any number of positional and keyword arguments using *args and **kwargs. In this article, it’s the turn of another flavour of argument. You’ll look at parameters that can only accept positional arguments and those that can only be keyword arguments. Let’s see how to create positional-only and keyword-only arguments in Python.

Overview Of The Intermediate Python Functions Series

Here’s an overview of the seven articles in this series:

  1. Introduction to the series: Do you know all your functions terminology well?
  2. Choosing whether to use positional or keyword arguments when calling a function
  3. Using optional arguments by including default values when defining a function
  4. Using any number of optional positional and keyword arguments: *args and **kwargs
  5. [This article] Using positional-only arguments and keyword-only arguments: the “rogue” forward slash / or asterisk * in function signatures
  6. Type hinting in functions
  7. Best practices when defining and using functions

Positional-Only Arguments in Python

When looking at documentation, you may have seen a “rogue” forward slash / in a function signature such as this:

def greet_person(person, /, repeat):

Let’s explore this by starting with the following function, which is one you’ve already seen in earlier articles in this series. Note how the string is being multiplied by the integer repeat:

def greet_person(person, repeat):
    print(f"Hello {person}, how are you doing today?\n" * repeat)

# 1.
greet_person("Zahra", 2)

# 2.
greet_person("Zahra", repeat=2)

# 3.
greet_person(person="Zahra", repeat=2)

The function has two parameters:

  • person
  • repeat

All three function calls work:

Hello Zahra, how are you doing today?
Hello Zahra, how are you doing today?

Hello Zahra, how are you doing today?
Hello Zahra, how are you doing today?

Hello Zahra, how are you doing today?
Hello Zahra, how are you doing today?

You can choose to use positional arguments as in the first function call in the code above. The arguments are matched to parameters based on their position in the function call.

You can choose to use named arguments (also called keyword arguments) as in the third function call above. Both arguments are named using the parameter name as a keyword when calling the function.

You can even use a mixture of positional and keyword arguments as long as the positional arguments come first. This is the case in the second function call above.

You can refresh your memory about positional and keyword arguments in the first article in this series.

Adding the “rogue” forward slash to the function signature

Let’s make one small change to the function definition by adding a lone forward slash among the parameters:

def greet_person(person, /, repeat):
    print(f"Hello {person}, how are you doing today?\n" * repeat)

You’ve added the forward slash between the two parameters. Now, you can try calling the function in the same three ways as in the previous section.

#1.

This is the scenario in which both arguments are positional:

def greet_person(person, /, repeat):
    print(f"Hello {person}, how are you doing today?\n" * repeat)

# 1.
greet_person("Zahra", 2)

This works fine. Here’s the output:

Hello Zahra, how are you doing today?
Hello Zahra, how are you doing today?

In this scenario, the forward slash didn’t affect the output. We’ll get back to this first example shortly.

#2.

In this example, you call the function with one positional argument and one keyword argument. The first argument is positional, and the second one is named (keyword):

def greet_person(person, /, repeat):
    print(f"Hello {person}, how are you doing today?\n" * repeat)

# 2.
greet_person("Zahra", repeat=2)

Again, there are no issues when we run this code:

Hello Zahra, how are you doing today?
Hello Zahra, how are you doing today?

So far, so good. But let’s look at the third example now.

#3.

In this call, both arguments are named (keyword) arguments:

def greet_person(person, /, repeat):
    print(f"Hello {person}, how are you doing today?\n" * repeat)

# 3.
greet_person(person="Zahra", repeat=2)

When you run this code, it raises an error:

Traceback (most recent call last):
  File "...", line 5, in <module>
    greet_person(person="Zahra", repeat=2)
TypeError: greet_person() got some positional-only arguments passed as keyword arguments: 'person'

Let’s read the error message in the last line:

greet_person() got some positional-only arguments passed as keyword arguments: 'person'

The error message mentions a positional-only argument. This is because the parameter person can only accept positional arguments, not keyword arguments.

The forward slash / in the function signature is the point where the change occurs. Any argument assigned to parameters before the forward slash / can only be passed as positional arguments.

The parameters after the forward slash can accept both positional and keyword arguments. This is why the first and second function calls worked fine.

Moving the forward slash

Let’s move the forward slash / to the end of the parameters in the function call:

def greet_person(person, repeat, /):
    print(f"Hello {person}, how are you doing today?\n" * repeat)

And let’s look at the same function calls you used earlier.

#1.

Both arguments are positional arguments in this example:

def greet_person(person, repeat, /):
    print(f"Hello {person}, how are you doing today?\n" * repeat)

# 1.
greet_person("Zahra", 2)

This code works fine:

Hello Zahra, how are you doing today?
Hello Zahra, how are you doing today?

Let’s recall the rule about the forward slash / in the function signature. All parameters before the forward slash / must be assigned to positional arguments and cannot be keyword arguments. These are positional-only arguments. In this example, both arguments are positional. Therefore, this works.

#2.

In the second example, the second argument is a keyword argument:

def greet_person(person, repeat, /):
    print(f"Hello {person}, how are you doing today?\n" * repeat)

# 2.
greet_person("Zahra", repeat=2)

This raises an error since repeat cannot be assigned a keyword argument:

Traceback (most recent call last):
  File "...", line 12, in <module>
    greet_person("Zahra", repeat=2)
TypeError: greet_person() got some positional-only arguments passed as keyword arguments: 'repeat'

#3.

In the final example, both arguments are keyword arguments:

def greet_person(person, repeat, /):
    print(f"Hello {person}, how are you doing today?\n" * repeat)

# 3.
greet_person(person="Zahra", repeat=2)

This also raises an error:

Traceback (most recent call last):
  File "...", line 21, in <module>
    greet_person(person="Zahra", repeat=2)
TypeError: greet_person() got some positional-only arguments passed as keyword arguments: 'person, repeat'

Note that the error message now lists both person and repeat as parameters that have been assigned keyword arguments. In the second example, only repeat was listed in the error message.

Summary for positional-only arguments

When you define a function, you can force the user to use positional-only arguments for some of the arguments:

  • You can add a forward slash / as one of the parameters in the function definition
  • All arguments assigned to parameters before the / must be positional arguments

It’s up to you as a programmer to decide when to use positional-only arguments when defining functions. You may feel that restricting the user to positional-only arguments makes the function call more readable, neater, or less likely to lead to bugs. You’ll look at another example later in this article which illustrates such a case.

Keyword-Only Arguments in Python

Another “rogue” symbol you may see when reading documentation or code is the asterisk * as in this example:

def greet(host, *, guest):

Let’s work our way to this version of the function by starting with a simpler one which doesn’t have the asterisk:

def greet(host, guest):
    print(f"{host} says hello to {guest}")

# 1.
greet("James", "Claire")

# 2.
greet(host="James", guest="Claire")

# 3.
greet(guest="Claire", host="James")

The function has two parameters, and you can use either positional arguments or keyword arguments when calling this function. When you run this code, you’ll see that all three function calls work:

James says hello to Claire
James says hello to Claire
James says hello to Claire

You can add an asterisk to the function definition. You can start by adding it at the beginning:

def greet(*, host, guest):
    print(f"{host} says hello to {guest}")

Let’s look at the three function calls in the example above. You’ll explore them in reverse order starting from the third one.

#3.

In this example, both arguments are keyword arguments:

def greet(*, host, guest):
    print(f"{host} says hello to {guest}")

# 3.
greet(guest="Claire", host="James")

Note that the arguments in the function call are not in the same order as the parameters in the function definition. You’ve seen earlier in this series that you can do this when using keyword arguments. The order of the arguments is no longer needed to assign arguments to parameters. The output from this code is:

James says hello to Claire

#2.

The second function call is very similar to the third. Both arguments are keyword (named) arguments, but the order is different:

def greet(*, host, guest):
    print(f"{host} says hello to {guest}")

# 2.
greet(host="James", guest="Claire")

The output is the same as in #3:

James says hello to Claire

#1.

In the first function call, both arguments are now positional arguments:

def greet(*, host, guest):
    print(f"{host} says hello to {guest}")

# 1.
greet("James", "Claire")

In this case, we encounter a problem:

Traceback (most recent call last):
  File "...", line 5, in <module>
    greet("James", "Claire")
TypeError: greet() takes 0 positional arguments but 2 were given

The error message says:

greet() takes 0 positional arguments but 2 were given

Let’s dive a bit deeper into what this error message is saying. This function cannot take any positional arguments. Both arguments must be keyword (or named) arguments. The asterisk * in the function definition forces this behaviour. Any argument assigned to parameters after the asterisk * can only be passed as keyword (named) arguments.

You can see why forcing keyword-only arguments can be beneficial in this case. The function takes two people’s names as arguments, but it’s important to distinguish between the host and the guest. Forcing keyword-only arguments can minimise bugs as the user doesn’t need to remember or check every time who comes first in the function call, the host or the guest.

Moving the asterisk to a different position

Let’s reinforce what’s happening by making one more change and placing the asterisk * in between the two parameters:

def greet(host, *, guest):
    print(f"{host} says hello to {guest}")

You can try the three function calls again. Let’s lump #2. and #3. together.

#2. and #3.

Both arguments are keyword arguments in these two calls:

def greet(host, *, guest):
    print(f"{host} says hello to {guest}")

# 2.
greet(host="James", guest="Claire")

# 3.
greet(guest="Claire", host="James")

The output is the same from both calls:

James says hello to Claire
James says hello to Claire

#1.

In the first function call, both arguments are positional:

def greet(host, *, guest):
    print(f"{host} says hello to {guest}")

# 1.
greet("James", "Claire")

This raises an error:

Traceback (most recent call last):
  File "...", line 5, in <module>
    greet("James", "Claire")
TypeError: greet() takes 1 positional argument but 2 were given

The error message states:

greet() takes 1 positional argument but 2 were given

The asterisk * forces the parameters which come after it to be keyword-only arguments. Therefore, guest must be assigned a keyword-only argument. The parameter host can be a positional argument.

The problem in this call is with the second argument "Claire", and not with the first one "James". You can confirm this with a fourth example.

#4.

In this call, the first argument is positional and the second is a keyword argument:

def greet(host, *, guest):
    print(f"{host} says hello to {guest}")

# 4.
greet("James", guest="Claire")

The output is:

James says hello to Claire

You can pass either a positional or a keyword argument to host, which comes before the asterisk *. However, you can only pass a keyword (named) argument to guest, which comes after the asterisk *.

Summary for keyword-only arguments

When you define a function, you can force the user to use keyword-only arguments for some of the arguments:

  • You can add an asterisk * as one of the parameters in the function definition
  • All arguments assigned to parameters after the * must be keyword (named) arguments

Positional-Only And Keyword-Only Arguments in Python

Let’s finish this article with another example which combines both positional-only and keyword-only arguments in the same function.

Look at the function definition and the four function calls below. Do you think any of them will raise an error?

def greet(greeting, /, repeat, *, host, guest):
    for _ in range(repeat):
        print(f"{host} says '{greeting}' to {guest}")

# 1.
greet("Hello!", 3, "James", "Claire")

# 2.
greet("Hello!", 3, host="James", guest="Claire")

# 3.
greet("Hello!", repeat=3, host="James", guest="Claire")

# 4.
greet(greeting="Hello", repeat=3, host="James", guest="Claire")

Note that the function definition has both a / and an *. Let’s explore all four function calls.

#1.

All four arguments are positional arguments in this function call:

def greet(greeting, /, repeat, *, host, guest):
    for _ in range(repeat):
        print(f"{host} says '{greeting}' to {guest}")

# 1.
greet("Hello!", 3, "James", "Claire")

This raises an error:

Traceback (most recent call last):
  File "...", line 6, in <module>
    greet("Hello!", 3, "James", "Claire")
TypeError: greet() takes 2 positional arguments but 4 were given

The part of the function signature which leads to this error is the asterisk *. Any parameter after the asterisk * must be matched to a keyword-only argument. Therefore the arguments "James" and "Claire" lead to this error. This function can take at most two positional arguments, as mentioned in the error message:

greet() takes 2 positional arguments but 4 were given

You can confirm that it’s the asterisk * which causes the problem by removing it and calling the same function:

# Version without an *

def greet(greeting, /, repeat, host, guest):
    for _ in range(repeat):
        print(f"{host} says '{greeting}' to {guest}")

# 1. (no asterisk in signature)
greet("Hello!", 3, "James", "Claire")

This version which doesn’t have the asterisk * works:

James says 'Hello!' to Claire
James says 'Hello!' to Claire
James says 'Hello!' to Claire

#2.

You’ll go back to the function definition with both a / and an * and look at the second call:

def greet(greeting, /, repeat, *, host, guest):
    for _ in range(repeat):
        print(f"{host} says '{greeting}' to {guest}")

# 2.
greet("Hello!", 3, host="James", guest="Claire")

The first two arguments are positional:

  • "Hello" is assigned to greeting
  • 3 is assigned to repeat

The third and fourth arguments are keyword (named) arguments:

  • host="James"
  • guest="Claire"

The output from this code is:

James says 'Hello!' to Claire
James says 'Hello!' to Claire
James says 'Hello!' to Claire

You’ve seen earlier that only the last two arguments are forced to be keyword-only arguments by the asterisk * which precedes them. Therefore, this function call works perfectly fine.

#3.

Let’s look at the third call:

def greet(greeting, /, repeat, *, host, guest):
    for _ in range(repeat):
        print(f"{host} says '{greeting}' to {guest}")

# 3.
greet("Hello!", repeat=3, host="James", guest="Claire")

Only the first argument is positional:

  • "Hello" is assigned to greeting

The rest are keyword (named) arguments:

  • repeat=3
  • host="James"
  • guest="Claire"

This works. Here’s the output:

James says 'Hello!' to Claire
James says 'Hello!' to Claire
James says 'Hello!' to Claire

The last two arguments must be keyword arguments since the parameters host and guest come after the asterisk * in the function signature.

The parameter repeat is “sandwiched” between the forward slash / and the asterisk * in the function signature. This means the argument is neither keyword-only nor positional-only. It’s not keyword-only because it comes before the asterisk *. It’s not positional-only because it comes after the forward slash /. This is a “normal” parameter and the argument passed to it can be either positional or keyword.

#4.

In this final example, all arguments are keyword arguments:

def greet(greeting, /, repeat, *, host, guest):
    for _ in range(repeat):
        print(f"{host} says '{greeting}' to {guest}")

# 4.
greet(greeting="Hello", repeat=3, host="James", guest="Claire")

This raises an error:

Traceback (most recent call last):
  File "...", line 6, in <module>
    greet(greeting="Hello", repeat=3, host="James", guest="Claire")
TypeError: greet() got some positional-only arguments passed as keyword arguments: 'greeting'

The argument must be a positional one since greeting is before the forward slash / in the function definition.

Final Words

In summary, you can define a function to have some positional-only arguments, some keyword-only arguments, and some arguments that could be either positional or keyword:

  • Parameters before the forward slash /: You must use positional-only arguments
  • Parameters after the asterisk * : You must use keyword-only arguments
  • Parameters in between / and * : You can use either positional arguments or keyword arguments

The following pseudo-definition summarises these points:

def template(positional_only, /, positional_or_keyword, *, keyword_only):

I’ll add a bit more in an appendix to help you remember which of the / or * symbols does what.

This concludes our discussion about the various types of arguments you can have in a Python function. In the remaining articles in this series, you’ll read about type hinting and best practices when defining and using functions.

Next Article: Using type hints when defining a Python function

Further Reading

Appendix: How to Remember Which Symbol Does What

I sometimes struggle to recall which of the / or * symbols does what. To remember which is which, you can note that the asterisk * is the same symbol you use to create *args. In fact, you could replace the * with *args and the function will work in a similar manner:

def greet(greeting, /, repeat, *args, host, guest):
    for _ in range(repeat):
        print(f"{host} says '{greeting}' to {guest}")

# 2.
greet("Hello!", 3, host="James", guest="Claire")

The output is:

James says 'Hello!' to Claire
James says 'Hello!' to Claire
James says 'Hello!' to Claire

The *args parameter is “mopping up” all remaining positional arguments before the keyword arguments. Therefore, all parameters after the *args must be used with keyword arguments. Recall that keyword arguments always come after positional ones in a function call.

In this example, there are no additional positional arguments to “mop up”, so args is an empty tuple. Note that when using *args as in this example, all arguments before the *args need to be positional so the / is no longer needed in this case. Using * instead of *args gives you more flexibility as you can have arguments which are either positional or keyword preceding it.


Get the latest blog updates

No spam promise. You’ll get an email when a new blog post is published


5 thoughts on “Using Positional-Only And Keyword-Only Arguments in Python [Intermediate Python Functions Series #5]”

Leave a Reply