Function Definition with Keyword-Only Parameters

by Christoph Schiessl on [Python]

The opposite of position-only parameters is keyword-only parameters, and they are equally or maybe even more common in the Python world. As you may have already guessed, functions can define specific parameters as keyword-only and thereby require the caller of those functions to provide these parameters using keyword notation. There is a special syntax for this, using the * character:

def foo(a, *, b):
    pass

The * has the following effect: All parameters to the right of the * must be provided using keyword notation — in this case, the only such parameter is b. Parameters to the left of the * are not affected, meaning their default behavior is still active. As a quick reminder: Default behavior means the caller can decide if he prefers positional or keyword notation.

Similarly, we can define functions that accept only keyword parameters:

def bar(* a, b):
    pass

The function above has two parameters, a and b, both of which must be provided using keyword notation. For the sake of completeness, here is the grammar from the official Language Reference:

funcdef                   ::=  [decorators] "def" funcname [type_params] "(" [parameter_list] ")"
                               ["->" expression] ":" suite
decorators                ::=  decorator+
decorator                 ::=  "@" assignment_expression NEWLINE
parameter_list            ::=  defparameter ("," defparameter)* "," "/" ["," [parameter_list_no_posonly]]
                                 | parameter_list_no_posonly
parameter_list_no_posonly ::=  defparameter ("," defparameter)* ["," [parameter_list_starargs]]
                               | parameter_list_starargs
parameter_list_starargs   ::=  "*" [parameter] ("," defparameter)* ["," ["**" parameter [","]]]
                               | "**" parameter [","]
parameter                 ::=  identifier [":" expression]
defparameter              ::=  parameter ["=" expression]
funcname                  ::=  identifier

What we are talking about is the line that defines parameter_list_starargs. The grammar tells us about the allowed syntax, but it doesn't explain the semantics, meaning it tells us where a * can occur in the parameter list, but it doesn't explain how this affects the function's caller. In any case, the line defining parameter_list_no_posonly references parameter_list_starargs, which means that the * syntax can be used in a longer list of parameters as we already demonstrated when we defined foo(a, *, b).

So far, so good. Let's see it in action now ...

Python 3.12.1 (main, Jan  1 2024, 16:22:47) [GCC 13.2.1 20230801] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> def foo(a, *, b):
...     pass
...
>>> foo(1, b=2)   # `b` must be supplied as a keyword parameter
>>> foo(a=1, b=2) # `a` can be supplied as a positional and a keyword parameter
>>> foo(1, 2)     # TypeError if you try passing `b` as a positional parameter
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: foo() takes 1 positional argument but 2 were given
>>> def bar(*, a, b):
...     pass
...
>>> bar(a=1, b=2) # `a` and `b` must be supplied as keyword parameters
>>> bar(1, b=2)   # TypeError if you try passing `b` as a positional parameter
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: bar() takes 0 positional arguments but 1 positional argument (and 1 keyword-only argument) were given
>>> bar(1, 2)     # TypeError if you try passing `a` and `b` as positional parameters
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: bar() takes 0 positional arguments but 2 were given

One edge case that I can think of is a * without any parameters to its right:

Python 3.12.1 (main, Jan  1 2024, 16:22:47) [GCC 13.2.1 20230801] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> def baz(a, *) # SyntaxError: At least one parameter must be to the right of the `*`.
  File "<stdin>", line 1
    def baz(a, *)
               ^
SyntaxError: named arguments must follow bare *

It makes intuitive sense that this is not allowed. Think about it. What would the purpose of the * in this function be? It wouldn't accomplish anything.

Interplay with Position-Only Parameters

You can, of course, combine keyword-only and position-only parameters, as long as you keep certain things in mind:

Python 3.12.1 (main, Jan  1 2024, 16:22:47) [GCC 13.2.1 20230801] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> def foo(a, /, b, *, c):
...     pass
...
>>> foo(1, 2, 3)       # TypeErorr because `c` comes after the `*`, and therefore, it's keyword-only
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: foo() takes 2 positional arguments but 3 were given
>>> foo(1, 2, c=3)
>>> foo(1, b=2, c=3)
>>> foo(a=1, b=2, c=3) # TypeError: `a` comes before the `/`, and therefore, it's  positon-only
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: foo() got some positional-only arguments passed as keyword arguments: 'a'

What's not allowed is to put a * before the /. But, it's valid to put a * immediately after a / to define a function that leaves no flexibility to the caller.

Python 3.12.1 (main, Jan  1 2024, 16:22:47) [GCC 13.2.1 20230801] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> def bar(a, /, *, b):
...     pass
...
>>> bar(1, b=2)
>>> bar(1, 2)     # TypeErorr because `b` comes after the `*`, and therefore, it's keyword-only
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: bar() takes 1 positional argument but 2 were given
>>> bar(a=1, b=2) # TypeError: `a` comes before the `/`, and therefore, it's  positon-only
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: bar() got some positional-only arguments passed as keyword arguments: 'a'

Thank you very much for reading, and see you soon! Please don't hesitate to reach out if you have any questions.