Search code examples
pythonargumentspython-3.8default-argumentspep570

Corner case with positional-only parameters in Python 3.8?


I am fiddling around with positional-only parameters as specified in PEP 570 and introduced with Python 3.8, and I was just wondering about a specific corner case.

Let's say I define a function as follows (no matter whether that is good design or makes any sense at all):

def func(p1, p2=None, p3=None, /): 
    print(p1, p2, p3)

So there is one required parameter (p1), followed by two optional parameters (p2 and p3). I can call the function with just p1, p1 and p2 or p1 and p2 and p3:

func(1)       # 1, None, None
func(1, 2)    # 1, 2, None
func(1, 2, 3) # 1, 2, 3

But there is no way I can ever just call it with p1 and an argument for p3 while keeping the default for p2, as I can not provide keyword arguments:

func(1, p3=3)

This will of course raise a TypeError:

TypeError: func() got some positional-only arguments passed as keyword arguments: 'p3'

I couldn't find any discussion or examples on this case, as all of the examples in PEP 570 just cover a single optional parameter as part of the positional-only arguments:

def name(p1, p2, /, p_or_kw, *, kw):
def name(p1, p2=None, /, p_or_kw=None, *, kw):
def name(p1, p2=None, /, *, kw):
def name(p1, p2=None, /):
def name(p1, p2, /, p_or_kw):
def name(p1, p2, /):

So my question is: Is that the intended behavior, to have a caller provide multiple optional arguments from left to right, overriding them in a forced order? Is this actually a feature of positional-only arguments?


Solution

  • Is that the intended behavior, to have a caller provide multiple optional arguments from left to right, overriding them in a forced order? Is this actually a feature of positional-only arguments?

    Not only that is the "intended behavior" of positional-arguments, it's pretty much the definition of it.

    func(1, p3=3) directly contradicts the use of / in the function's signature as it provides a keyword argument to a function that accepts only positional arguments. The fact that p2 has a default value is irrelevant (though it's pretty much useless as you found).

    I will keep looking for an explicit explanation in the documentation but there might not be one. It's basically a straightforward implication of using /.

    However, PEP570 includes this example:

    def name(positional_only_parameters, /, positional_or_keyword_parameters,
             *, keyword_only_parameters):
    

    Which suggests we can rewrite func as:

    def func(p1,p3=None, /, p2=None):
        print(p1, p2, p3)
    

    Then both of these work:

    func(1, 3)
    func(1, 3, 2)
    

    Output is

    1 None 3
    1 2 3
    

    Try it online