Currently, Python doesn't allow non-default arguments after default arguments:

>>> def foo(x=None, y): pass
  File "<stdin>", line 1
    def foo(x=None, y): pass
                     ^
SyntaxError: non-default argument follows default argument

I believe that at the time this was introduced, no use cases for this were known and this is is supposed to prevent a source of bugs. I have two use cases for this, one fringe, but valid, the other more important:

The fringe use case: Suppose you have a function that takes a 2D coordinate value as separate "x" and "y" arguments. The "x" argument is optional, the "y" argument isn't. Currently there are two ways to do this, none of them particularly great:

def foo(y, x):  # reverse x and y arguments, confusing
    ...
def foo(x, y=None):  # Treat the x argument as y if only one argument is provided
    if y is None:
        x, y = y, x
    ...

To me, the "natural" solution looks like this:

def foo(x=None, y): ...
# Called like this:
foo(1, 2)
foo(y=2)

This could also be useful when evolving APIs. For example there is a function "bar" that takes two required arguments. In a later version, the first argument gains a useful default, the second doesn't. There is no sensible way to evolve the API at the moment.

The more important use case involves @overloads. A condensed example of a function where the return type depends on an "encoding" parameter, followed by further parameters that could be called like this:

foo(123, "utf-8")  # would return bytes
foo(encoding="utf-8")
foo(123, None)  # would return str
foo(encoding=None)
foo(x=123)  # would return str
foo()

This could ideally be written as:

@overload
def foo(x: int = ..., encoding: None = ...) -> str: ...
@overload
def foo(x: int = ..., encoding: str) -> bytes: ...
# plus the actual implementation

But due to the syntax constraint, this needs to be hacked around with a third overload:

@overload
def foo(x: int = ... encoding: None = ...) -> str: ...
@overload
def foo(x: int, encoding: str) -> bytes: ...  # for foo(123, "utf-8")
@overload
def foo(*, encoding: str) -> bytes: ...  # for foo(encoding="utf-8")

Not only is this hard to read, real examples in typeshed are usually more complex, with many arguments before or after the affected argument or even multiple affected arguments. This often becomes too complex to write or maintain. Here is one example from the wild: https://github.com/python/typeshed/blob/b95b729b9e07ab21d252701af0f5b7404672b952/stubs/redis/redis/client.pyi#L51

Allowing non-default arguments after default arguments would solve both use cases above and eliminates a special case. I'm also not sure what exactly the current SyntaxError really protects us from. Adding a non-default after a default argument can't really lead bugs.

 - Sebastian

_______________________________________________
Python-Dev mailing list -- python-dev@python.org
To unsubscribe send an email to python-dev-le...@python.org
https://mail.python.org/mailman3/lists/python-dev.python.org/
Message archived at 
https://mail.python.org/archives/list/python-dev@python.org/message/7W5RMJVBRN6NE6A3LUP5Y4BMOZKQRYWH/
Code of Conduct: http://python.org/psf/codeofconduct/

Reply via email to