|
|
Subscribe / Log in / New account

Positional-only parameters for Python

Please consider subscribing to LWN

Subscriptions are the lifeblood of LWN.net. If you appreciate this content and would like to see more of it, your subscription will help to ensure that LWN continues to thrive. Please visit this page to join up and keep LWN on the net.

By Jake Edge
April 10, 2019

Arguments can be passed to Python functions by position or by keyword—generally both. There are times when API designers may wish to restrict some function parameters to only be passed by position, which is harder than some think it should be in pure Python. That has led to a PEP that is meant to make the situation better, but opponents say it doesn't really do that; it simply replaces one obscure mechanism with another. The PEP was assigned a fairly well-known "BDFL delegate" (former BDFL Guido van Rossum), who has accepted it, presumably for Python 3.8.

Background

Since Python 1.0 or so, parameters to Python functions can be passed as positional arguments or as keyword arguments. For example:

    def fun(a, b, c=None):
        ...

    fun(1, 2)
    fun(1, 2, 3)
    fun(a=1, b=2, c=3)
    fun(c=3, a=1, b=2)

The function fun() takes two positional arguments and one optional argument, which defaults to None. All four of the invocations of the function shown above are legal as well. The only restriction is that positional arguments, those without a "keyword=", must all come before any keyword arguments when calling a function. So "fun(a=1, 2)" will raise a SyntaxError.

As can be seen above, any of the parameters can be passed as a keyword argument, even if the function author did not expect them to be. That means that changing the parameter name down the road, due to refactoring or for more clarity, say, may cause callers to fail if they are using the old name. That places more of a burden to come up with a "meaningful" name, even when there may be no real reason to do so (e.g. min(arg1, arg2)).

Positional-only parameters

It would be nice if library authors could indicate which parameter names are meant to be used only by positional arguments. In fact, some CPython builtins and standard library functions written in C are already able to specify and enforce positional-only arguments. Looking at help(pow) in the Python interpreter will show the function signature as follows:

    pow(x, y, z=None, /)

The "/" is a documentation convention that originated in the Python "Argument Clinic", which is a preprocessor to generate argument-handling code for CPython builtins. The "/" separates positional-only arguments from those that can be either positional or keyword (though the convention is not used in the pow() entry in the online documentation). Trying to call pow() in any of the following ways will lead to a TypeError being raised:

    pow(2, 4, z=5)
    pow(x=5, y=7)

PEP 570 ("Python Positional-Only Parameters") seeks to make that convention an actual part of the language syntax. As the PEP notes, using *args can accomplish the same goal, but it obscures the function's "true" signature. In a function definition, *args acts as a tuple that collects up any positional arguments that have not been consumed by earlier positional parameters. (The documentation for function-parameter syntax in Python is a bit scattered, as pointed out by this helpful blog post, which summarizes that information.)

Using a symbol to separate different kinds of parameters is an already-established precedent in Python. In 2006, PEP 3102 ("Keyword-Only Arguments") described using "*" in parameter lists to indicate that any following parameters must be specified as keywords. As noted in that PEP, one could emulate the * by using a dummy parameter (e.g. *dummy) to collect up any remaining positional arguments, but if that isn't an empty tuple, the function has been called incorrectly. Rather than force users to add a dummy parameter and test it for emptiness, * was added to have the same effect.

    def fun(a, b, *dummy, kword=None):
        if dummy:
	    raise TypeError
	...

    # becomes:

    def fun(a, b, *, kword=None):
        ...

PEP 570 extends that idea to a certain extent. As described in the "Specification" section, it would work as follows:

From the "ten-thousand foot view", eliding *args and **kwargs for illustration, the grammar for a function definition would look like:
    def name(positional_or_keyword_parameters, *, keyword_only_parameters):

Building on that example, the new syntax for function definitions would look like:

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

There are some performance benefits to positional-only parameters, so providing pure-Python functions with a way to specify them would be helpful. In addition, since different Python implementations make their own choices about what language to use for standard library functions, inconsistencies can arise. Pure-Python implementations of standard library functions cannot exactly match the behavior of C-based functions due to the lack of positional-only parameters. Alternatives to CPython should not have to jump through hoops to emulate positional-only parameters simply because they have a chosen a pure-Python implementation for some standard library function.

Another thing to consider, according to the PEP, is consistency for subclasses. If a base class defines a method using one parameter name that is intended to be positional and a subclass uses a different name, calls using the parent class's parameter name as a keyword argument will fail for the subclass, as the PEP's example shows. Adding a positional-only parameter will remove that problem. Beyond that, there is a corner case that can be cleaned up:

    def fun(name, **kwords):
        return 'name' in kwords       # always False

    def fun(name, /, **kwords):
        return 'name' in kwords       # True for fun(a, name=foo)

This corner case also plays out in other scenarios. If a function uses a name for a parameter, that precludes callers from using it as a keyword argument elsewhere in the argument list. An example using the str.format_map() builtin:

    def fun(fmt, **kwords):
        fmt.format_map(kwords)

    fun('format: {fmt}', fmt='binary')  # TypeError because fmt is reused

If fun() could be defined using the proposed syntax, the "reuse" of fmt would not cause an error:

    def fun(fmt, /, **kwords):
        ...

In a long discussion on the Python Discourse instance (which, incidentally, demonstrates the deficiencies of Discourse's unthreaded discussion, at least for me), the idea was hashed out. Much of the objection turned out to be the use of "/", it seems. That bit of syntax was seen as ugly and/or unnecessary, but its heritage goes a fair ways back. It originated from Van Rossum in a 2012 python-ideas post; he pointed out that / is kind of the opposite of * (which is used to mark keyword-only parameters) in some contexts (e.g. Python arithmetic). No one, including Van Rossum, is entirely happy with using /, but no one has come up with anything less ugly—at least in his view.

That's not for lack of trying. The use case has been discussed multiple times along the way but, even just this time, there were suggestions for using a decorator-based approach, repurposing Python 2 tuple-unpacking parameters, or using double-underscore prefixes on parameters to mark them as positional-only. All of those were rejected for various reasons, which are described in the PEP. As might be guessed, the reasons are often not entirely convincing to the proponents of those ideas.

No change desired

Either leaving things as they are or, perhaps, even changing the C-based functions to accept all arguments as keywords were suggested in the discussion. Raymond Hettinger said that the / notation used in the runtime documentation for builtins has been a failure. He is strongly opposed to adding it as real syntax, at least in part because it will be difficult to teach. He is concerned that it is a fairly minor problem being solved:

[...] I can report that the “/” notation in the help() output and tooltips has been an abject failure. It is not user friendly, necessary, or communicative.

Over time, we’ve had a trend of adding unnecessary, low-payoff complexity to the language. Cumulatively, it has greatly increased the mental load for newcomers and for occasional users. The help() output has become less self-explanatory over time and makes the language feel more complex. The proposal at hand makes it worse.

Pablo Galindo Salgado, who has been shepherding the PEP, unsurprisingly disagreed with Hettinger's complaints. There are valid problems that would be solved with the new syntax, Galindo Salgado said. In addition, the help() output would become more useful because it would always correspond with what can be used in a def statement, unlike the situation today.

Steve Dower had a more sweeping idea. He would like to see all parameters allowed as keyword arguments:

[...] I’d go as far as changing the builtins like range() to support named arguments no matter how ugly the implementation gets. Knowing that every argument can be specified by name is a feature, and I’m not convinced abandoning that is worth it.

But Van Rossum did not agree:

It’s not a feature. Readability Counts, and writing len(obj=configurations) is not something we want to encourage.

He is not particularly swayed by the "hard to teach" argument: "there are tons of advanced features that beginners don’t need to be taught". He also noted that adding the syntax that help() uses will help remove that confusion. Van Rossum made it clear that he believes the function's author should be in control of how the function is called, while Dower had the opposite view.

Along the way, Galindo Salgado pointed to a bug report from 2010 that could have been solved with positional-only parameters. In addition, Serhiy Storchaka created a work-in-progress pull request with a patch to change all positional-only parameters in the standard library once the new syntax is adopted. Van Rossum is not inclined to go quite that far, but does want to adopt the obvious cases.

As part of Storchaka's look into the standard library, he found a simple change that would automatically fix most or all of the corner-case variety of problems. As described in a bug report, the change would simply allow a keyword argument that duplicated a positional parameter name to be placed into **kwords. So the fmt example above would simply start working without the change in syntax. It is a fairly fundamental change to how the **kwords parameters work, however, so Van Rossum would like to see it get its own PEP and to discuss it separately.

The worries from Hettinger and others about teaching the new feature did lead Van Rossum to request that a new "How To Teach This" section be added to the PEP. That section is a draft for an addition to the "More on Defining Functions" section of the Python Tutorial. Steering committee member Carol Willing suggested adding some documentation helping to guide users on choosing between the various parameter types, along the lines of a blog post from Trey Hunner.

In truth, there was not much suspense about the outcome of this PEP. Fairly early on, Van Rossum tipped his hand in support of the PEP—well before the bulk of the thread was posted. It does seem like a useful addition to the language and one that can largely be ignored by those who don't need it. For those who do need it, though, it can make things a lot easier for certain types of functions.


Index entries for this article
PythonArguments
PythonPython Enhancement Proposals (PEP)/PEP 570


(Log in to post comments)

Positional-only parameters for Python

Posted Apr 10, 2019 6:14 UTC (Wed) by niner (subscriber, #26151) [Link]

Seems to me like the real solution would be to just not allow passing positional arguments by keyword, i.e. keeping them completely separate. That would get rid of the renaming issue, the subclassing issue, the "fmt" issue and all inconsistencies. It would also adhere more to the Python design principles, most of all to:
Special cases aren't special enough to break the rules.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
If the implementation is hard to explain, it's a bad idea.

Of course such a change would be quite backwards incompatible. If only they had used the one chance they got to make such changes for actually fixing the language...

Positional-only parameters for Python

Posted Apr 10, 2019 6:38 UTC (Wed) by foom (subscriber, #14868) [Link]

But that's exactly the problem: There's no such thing as a "positional argument" to a function defined as Python (vs C) until the new PEP.

Maybe you meant that all _required_ arguments should have been made positional only? But, then what about optional arguments? Those must allow keyword calls?

Positional-only parameters for Python

Posted Apr 10, 2019 11:02 UTC (Wed) by LtWorf (subscriber, #124958) [Link]

Breaking changes in languages are bad.

Positional-only parameters for Python

Posted Apr 10, 2019 10:59 UTC (Wed) by rgb (subscriber, #57129) [Link]

How about

def fun(name, *_):
...

Positional-only parameters for Python

Posted Apr 10, 2019 13:16 UTC (Wed) by FLHerne (guest, #105373) [Link]

That's already valid syntax - `_` is an acceptable name in Python, so it's identical to `def fun(name, *args)`.

>>> def fun(name, *_):
>>> print(_)
...
>>> fun("A", "B", "C")
('B', 'C')

Positional-only parameters for Python

Posted Apr 10, 2019 16:35 UTC (Wed) by dbaker (guest, #89236) [Link]

not only that, _ is the conventional name of any variables that you're going to ignore. Which is really handy for a lot of things;

iterating over containers of containers:

for a, b, _ in [(1, 'a', []), (2, 'b', [])]: ...

for functions that need to fulfill an interface:

def f1(f: str, *, opt: bool = False):
def f2(f: str, **_):

myval = [f(myval, opt=True) for f in [f1, f2]]

with the python3 partial container explosion syntax:

a, b, *_ = (1, 2, 3, 4)

and a bunch of other cases.

Positional-only parameters for Python

Posted Apr 10, 2019 17:57 UTC (Wed) by juliank (guest, #45896) [Link]

Using _ for things to ignore is fine until you want to introduce gettext as _ :D

Positional-only parameters for Python

Posted Apr 11, 2019 9:39 UTC (Thu) by mgedmin (subscriber, #34497) [Link]

And for extra fun, _ has a special meaning in the REPL (it contains the value of the last expression).

$ python3
Python 3.6.7 (default, Oct 22 2018, 11:32:17)
[GCC 8.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> 2 + 2
4
>>> print("hi")
hi
>>> _
4

Positional-only parameters for Python

Posted Apr 12, 2019 20:41 UTC (Fri) by dbaker (guest, #89236) [Link]

Which is just so weird to me :)

Positional-only parameters for Python

Posted Apr 10, 2019 20:52 UTC (Wed) by yootis (subscriber, #4762) [Link]

Why is it ever actually necessary to prevent the use of keyword arguments? The article mentions "what if the API changes", but to me that's all the more reason to use keyword arguments. At least you'd detect the problem. Forcing positional arguments sounds dangerous to me.

Positional-only parameters for Python

Posted Apr 10, 2019 22:08 UTC (Wed) by quotemstr (subscriber, #45331) [Link]

One problem is API signature ambiguity. Say I have a def foo(bar): return bar +1. People right now are allowed to call foo as foo(5) or foo(bar=5). Now suppose I want to refactor foo as "def foo(qux):return 1+qux". Is the change of the argument name a breaking API change? Only if w allow keyword argument specification for bar! Right now, we have to treat argument names as part of a function's signature, and I think that's too constricting.

Positional-only parameters for Python

Posted Apr 11, 2019 2:11 UTC (Thu) by k8to (guest, #15413) [Link]

It seems valuable to have a choice on whether to expose the names or not. However, there are many modules where the docs don't talk about the argument names, but keyword arguments make the consuming code vastly more readable.

I think the fact that the arg names are exposed by default is essentially a good accident that forces interfaces to be a bit more usable than they are by default.

Amusingly I went to a good deal of trouble in some code I made to make the args keyword-only without making the implementation of the callsites harder to read. I had explicit documentation of how calls were required to be made, but used inspection to enforce it. I could used def func(**kwargs) but manually coding all the dispatch would have been really unfortunate.

Positional-only parameters for Python

Posted Apr 19, 2019 19:32 UTC (Fri) by tildeswinton (guest, #131545) [Link]

This seems to be a proliferation of the current PEP trend of adding complex functionality for edge cases. I would almost argue that many of these newer PEPs are the Python equivalent of giving a TEDx talk or presenting at another well known conference: there seems to be a stronger desire to have it as a bullet point on their resume than a desire to leave a meaningful, lasting impact.


Copyright © 2019, Eklektix, Inc.
This article may be redistributed under the terms of the Creative Commons CC BY-SA 4.0 license
Comments and public postings are copyrighted by their creators.
Linux is a registered trademark of Linus Torvalds