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 `a` 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:
- A
/
in the parameter list makes all parameters to its left position-only parameters. - A
*
in the parameter list makes all parameters to its right keyword-only parameters. - Parameters between
/
and*
retain the default behavior (the caller can pick a notation).
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.