0

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?

ShadowRanger
  • 143,180
  • 12
  • 188
  • 271
hjs
  • 11
  • 3
  • 2
    `"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?"` I might not understand you correctly, but not only that is the "intended behavior" of positional-arguments, it's pretty much the definition of it. – DeepSpace Oct 30 '19 at 09:03
  • `func(1, p3=3)` directly contradicts the use of `/` in the function's definition as it provides a keyword argument to a function that accepts only positional arguments. The fact the `p2` has a default value is irrelevant – DeepSpace Oct 30 '19 at 09:06
  • > func(1, p3=3) directly contradicts the use of / yeah, that was just to illustrate that just overriding a single default parameter is not possible. I was just confused by multiple defaults in positional-only parameters, but seems to be the behavior you actually want to have. I just found no explicit reference to that behavior. – hjs Oct 30 '19 at 09:07
  • With positional-or-keyword parameters, there is a notion of "here are some meaningful defaults, you can override them individually if you want to" whereas with positional-only there are defaults too, but it is more like "here are some defaults, you can override them one by one, but only in the order we tell you too". I guess this is what confused me, that in a positional-only context default parameters have different semantics. But it makes perfect sense, I guess the question is answered. – hjs Oct 30 '19 at 09:14

2 Answers2

0

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

DeepSpace
  • 78,697
  • 11
  • 109
  • 154
  • 1
    Sure, it would also enable one to call it like `func(1, 3, p2=2)` and even `func(1, p2=2)`. But that is a different kind of signature then, as it moves `p2` into the positional-or-keyword arguments. But you are right anyway, it is the intended behavior. I was just confused by the new semantics that multiple optional arguments have in a positional-only context, but it is actually what you want to have. – hjs Oct 30 '19 at 09:30
  • In a positional-or-keyword context, multiple optional arguments can be passed by the caller individually in any combination and order (like cherry picking), whereas in a positional-only context default parameters can be overriden, but only one by one, from left to right, in the order imposed by the signature. That is the point I wasn't grapsing before. – hjs Oct 30 '19 at 09:34
0

A good example for exactly the described behavior that got me confused is the eval Builtin, which has two positional-only optional arguments:

eval(source, globals=None, locals=None, /)
    Evaluate the given source in the context of globals and locals.

    The source may be a string representing a Python expression
    or a code object as returned by compile().
    The globals must be a dictionary and locals can be any mapping,
    defaulting to the current globals and locals.
    If only globals is given, locals defaults to it.

So there is no way to specifiy only the locals:

>>> eval("1 + 2 + x", locals={'x': 3})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: eval() takes no keyword arguments

Positionally, globals must be provided first (if locals are omitted, they default to the globals anyway):

>>> eval("1 + 2 + x", {'x': 3}) # globals = locals
6

... or, if they should differ:

>>> eval("1 + 2 + x", {'x': 3}, {'x': 4})
7

So, to answer the question: This is exactly the intended behavior.

hjs
  • 11
  • 3