Function Definition with Position-Only Parameters

by Christoph Schiessl on [Python]

Another interesting trick you see pretty often in the standard library is functions requiring the caller to provide certain parameters using positional notation. To define functions with this requirement, there is a special syntax — using a / in the parameter list. For instance, the parameter a in the following function must be provided as a positional parameter:

def foo(a, /, b):
    pass

Note that the parameter b is not affected at all by the /. For b, the default behavior is still active, meaning the caller can decide if he wants to provide the parameter using positional notation or keyword notation. If we look at the grammar from the official Language Reference again, we can see this in the line that defines parameter_list:

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

The expression defparameter ("," defparameter)* "," "/" ["," [parameter_list_no_posonly]] defines a list of one or more position-only parameters, optionally followed by all other parameters. The word "optionally" is important because this tells us that the following is also allowed:

def bar(a, b, /):
    pass

Namely, functions that accept only positional parameters and nothing else are also valid. So much for the theory — let's take this new knowledge for a spin:

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, 2)
>>> foo(1, b=2)
>>> foo(a=1, b=2) # doesn't work, because `a` is position-only!
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: foo() got some positional-only arguments passed as keyword arguments: 'a'
>>> def bar(a, b, /):
...     pass
...
>>> bar(1,2)
>>> bar(a=1, b=2) # doesn't work, because `a` and `b` are both position-only!
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: bar() got some positional-only arguments passed as keyword arguments: 'a, b'

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

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)
  File "<stdin>", line 1
    def baz(/, a)
            ^
SyntaxError: at least one argument must precede /

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 Default Parameters

Of course, it's still possible to provide default values for position-only parameters, but not for all of them. The restriction still applies: All parameters without default values must be listed before parameters with default values. Violating this rule results in an immediate SyntaxError:

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=1, /, b=2):
...     pass
...
>>> def bar(a=1, /, b):
  File "<stdin>", line 1
    def bar(a=1, /, b):
                    ^
SyntaxError: parameter without a default follows parameter with a default

That's everything for today. Thank you for reading, and see you soon!