
Positional-only Parameters for Python - l2dy
https://lwn.net/Articles/785245/
======
js2
> 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.

I couldn't agree more with this sentiment from Raymond Hettinger. I first
learned Python starting with 1.5.2, and coming from a background of C and
Perl. One of the things that really attracted me to the language was how
compact it was. It took no more than an hour to learn most of the language, a
day to read the entire spec, and maybe a second day to read the entire
standard library documentation. It suffered from virtually no gotchas...
nothing crazy like Perl's scalar vs list context, etc.

I haven't had trouble keeping pace with Python and I even probably agree with
most of its changes. But this change really seems superfluous. How does it fit
into the zen?

Can a language ever reach a point of being done?

~~~
red_hare
You're not alone. My goto example of this is the pathlib module added in 3.4.
It has this feature for making it easier to concat strings representing file
system paths...

    
    
        >>> from pathlib import Path
        >>> Path("foo/bar") / "baz"
        PosixPath('foo/bar/baz')
    

Don't get me wrong, the existing solutions until pathlib (os.path.join or
string concatenation) left a lot to be desired. And there are certain tricky
path-parsing things this module does that are wonderful. But overloading the
__div__ operator for a "cute" API just feels... wrong...

Of course, after my initial disgust, I now use it everywhere :)

~~~
okasaki
I'm not sure why it's wrong? Python isn't Haskell. If you're dealing with path
manipulation a lot, it makes for much more readable code.

~~~
jerf
"Python isn't Haskell."

Not sure exactly what you're saying there? Haskell strikes me as _far_ more
likely to define an operator for that.

Although Haskell will define something other than /, which unless you jump
through some hoops is going to be the fractional division operator [1]. You'd
have to be crazy to try to give a "path" an instance of Fractional, though I'd
be intrigued to hear someone's definition of the "reciprocal" of a path.

[1]:
[https://hackage.haskell.org/package/base-4.12.0.0/docs/Prelu...](https://hackage.haskell.org/package/base-4.12.0.0/docs/Prelude.html#t:Fractional)

~~~
twic
> I'd be intrigued to hear someone's definition of the "reciprocal" of a path.
    
    
      ..

~~~
jerf
Reciprocal is its own inverse, so the reciprocal of a/b/c and d/e/f can't both
be "..".

The closest you could come is an anti-path, where if you are at a/b/c, you
could have c'/b'/a' that when combined together produced the root path of both
these things. You could then have things like b'/a, which represents "rise up
to the previous directory, which is named 'b', and descend into a" where the
reciprocal is a'/b. I'm done fiddling with this for now, but it's possible you
could work this into a sensible scheme with some concept of "reciprocal". It's
useless in practice because who wants a .. operator that asserts the name of
the directory it is rising from? (And presumably, what, errors if it isn't
right? Refuses to rise like .. does at the root?)

However since Fractional instances also have to be Num instances [1], and
there's no practical way to fill in most of those, it's not going to get you
very far. And the Fractional requirement of fromRational, while you can try to
write something that turns a Rational (which in Haskell is specifically two
Integers being used as a ratio) into a path, it's going to be very silly.

[1]:
[https://hackage.haskell.org/package/base-4.12.0.0/docs/Prelu...](https://hackage.haskell.org/package/base-4.12.0.0/docs/Prelude.html#t:Fractional)

------
ak217
Keyword arguments are safer and more explicit/self-documenting. It's not clear
that further accommodations for positional arguments are needed, except in
variadic situations as described in
[https://www.python.org/dev/peps/pep-3102/](https://www.python.org/dev/peps/pep-3102/).
I would argue the use case described in the "background" section constitutes
an unsafe level of uncertainty.

One important use case for the existing facilities is to take a function whose
signature used to contain positionals, and migrate a positional to be a
keyword argument without breaking existing uses of the API.

I think `/` is a poor choice of symbol, too. If such a separator were to be a
good idea, it should be __* __* by analogy with `*` for separating keyword-
only args.

~~~
masklinn
> Keyword arguments are safer and more explicit/self-documenting.

Keyword arguments are also more redundant (a function taking a single argument
usually does not need that argument to be named, quite the opposite, even for
some multi-parameter functions it makes no sense e.g. `max(a=1, b=2)` is
downright no matter how you name the arguments)), and the lack of positional-
only parameters means any old name to what was intended as a positional
effectively becomes part of the API.

I think Swift has the better pick there, an argument can be _either_
positional or named, not both, and positional-only parameters are used to
great effect. Sadly Python's history of positional-or-named can't really be
fixed.

~~~
ehsankia
> a function taking a single argument usually does not need that argument to
> be named

Would you rather have:

thread_pool.shutdown(True)

or

thread_pool.shutdown(wait=True)

Even with single argument, it's not always clear what the argument means from
the function name, so being explicit makes the code more readable.

EDIT: I realize you said "usually", but my point still stands.

~~~
totalperspectiv
You can maintain clarity with your positional arguments with just:

wait = True

thread_pool.shutdown(wait)

~~~
polotics
I am sorry but this looks too much like Java to me.

------
alexbecker
Some people are saying the syntax is ugly, and I agree. But it addresses a
real problem in Python: the names of the arguments of your public functions
are also part of your public API (since they can be called as kwargs). In my
experience, this is rarely desirable and makes fixing bad names unnecessarily
hard.

~~~
masklinn
And worse, they cause problems in some APIs e.g. defining / overriding
MutableMapping.update(), it should take a single optional positional-only
parameter and any number of keyword arguments, currently this requires using
*args and hand-rolled assertions, otherwise you can get kwarg conflicts and
other such issues.

------
twic
This seems pretty nuts. I'm with Steve Dower here - make all parameters
allowed as keyword arguments.

Guido's objection that "writing len(obj=configurations) is not something we
want to encourage" makes not one iota of sense. Allowing it is not encouraging
it.

There's mention of one bug which allegedly could have been fixed with
positional-only parameters:

[https://bugs.python.org/issue9137](https://bugs.python.org/issue9137)

The bug here is that dicts have an update method which you can call two ways,
by passing a single dict, or with keyword arguments, and if you try to call
the single-dict version using a named parameter, it's not clear what should
happen, and at one point, for OrderedDict, it could end up throwing an
exception.

But the mistake here isn't confusing parameters, it's having a single method
that you can call in two different ways. That sort of thing might fly in Perl,
but it's a source of ambiguity and confusion that, IMHO, is not up to Python's
standards.

------
vharuck
The arguments for this change undervalue one of my favorite things about
Python: developer freedom. The language is versatile to the point of being
inefficient, but that lets me adapt it as necessary. Just because they can't
imagine a situation where this change would be a roadblock doesn't mean it
won't happen. What about building an argument list with a dictionary and then
passing it as an expanded dictionary?

Also, I don't like the idea that package developers' opinions on what's
"readable code" outrank mine, when we're talking about _my_ code.

------
ram_rar
The more and more I see the direction python is taking, the more it feels like
python will eventually become the new Perl. I honestly feel, if Python 2.7+
had "fixed" unicode and included orderedDict (which I guess came after 3.5) I
would happily stay with it.

~~~
Izkata
Python 2.7 has it... "from collections import OrderedDict"

------
kevin_thibedeau
If backward compatibility is a goal, one can't make capricious changes to the
order of positional args. It seems pointless to make special provisions to
prevent named args for the sake of "more flexibility" when positional is
decidedly less flexible.

You can always opt for dict args if you want to change a signature and
maintain backwards compatibility with obsolescent arg names.

If I was BDFL this would be a dead issue.

------
verisimilitudes
I do find having keyword parameters for free, as Ada does, to be an
interesting feature, but the need to make the parameter names part of the
interface is a drawback. Common Lisp separates positional parameters and
keywords, but at the cost of some efficiency and yet other drawbacks.

In general, though, I don't see the issue with always needing to pick good
names. It's very simple to use single-character names if one needs to, such as
for a mathematical function, or simply a procedure where the letters have
clear meanings or will likely never be used to start with.

Asides from the design features I thought were jokes when I first learned of
them, such as one-line lambdas and whatnot, this ''beginner's language''
certainly has a great deal of complicated syntax sugar and other things being
added to it.

------
xisukar
In Raku, parameters can be either positional or named parameters (declared
with the colon-pair syntax). However, from what I understand, only positional
parameters can be passed as positional and named parameters as named. The
closest Raku subroutine to Python's `def fun(a, b, c=None)` would be:

    
    
        sub fun($a, $b, :$c) { }
    
        fun(1, 2)                # This works!
        fun(1, 2, 3)             # Error: Too many positionals passed...
        fun(:a(1), :b(2), :c(3)) # Error: Too few positionals passed...
        fun(:c(3), :a(1), :b(2)) # Same as before
        fun(:c(3), 1, 2)         # This works!
       

Positional parameters are required by default but they can be made optional by
providing a default value in the subroutine's signature or by placing a `?`
right after the parameter. However, much like Python, positional parameters
must appear before optional parameters:

    
    
        sub fun($b, $a?, :$c) { }
        
        fun(1) # This works!
    

On the other hand, named parameter, as evidenced by the first code snippet,
are optional. However, they can be made required by placing a `!` right after
the parameter:

    
    
        sub fun($a, $b, :$c!) { }
        
        fun(1, 2)        # Error: Required named parameter 'c' not passed
        fun(1, 2, :c(3)) # This works!
    
    

Remaining arguments can be slurped by using slurpy parameters. In its most
basic form, slurpy positional parameters are indicated with `A` followed by an
array parameter:

    
    
        sub fun($a, $b, A@rest) { }
    

However, regardless of the structure of the arguments, they are flatten. This
automatic flattening can be avoided by using `AA` instead. Or it can be done
conditionally by using `+`: If there's a single argument, flaten it. Else,
leave them alone.

Slurpy named parameters are indicated also with `A` followed by a hash
parameter:

    
    
         sub fun(:$c, :$d, A%rest) { }
    

Both can be used in conjunction:

    
    
         sub fun($a, $b, :$c, :$d, A@rest, A%rest) { } # Or:
         sub fun($a, $b, A@rest, :$c, :$d, A%rest) { }
    

Edit: HN eats up the asterisks so using `A` to stand for asterisk.

------
amelius
There is a simple solution to this problem. Just name your parameters like
this:

    
    
        def fun(parameter1, parameter2, parameter3):
            a = parameter1
            b = parameter2
            c = parameter3
            ... (rest of function)
    

This also helps the user to see in an eyeblink which position corresponds to
which parameter.

------
r0f1
Is this a new thing? I thought that was possible in Python already. Look at
the numpy documentation:
[https://docs.scipy.org/doc/numpy/reference/generated/numpy.m...](https://docs.scipy.org/doc/numpy/reference/generated/numpy.maximum.html).
There, they already use the / for positional-only arguments.

~~~
masklinn
> Is this a new thing? I thought that was possible in Python already.

Yes and no: the syntax was used in documentation (that's the "argument clinic"
mentioned in the article) and Python functions defined in C were able to
define positional-only parameters, but it's a new thing for Python code:
before 3.8, it's not possible to define positional-only parameters in pure
Python[0], with it the "argument clinic" syntax becomes actual Python syntax
(rather than pseudo-python).

[0] well you can use *args and a bunch of hand-rolled assertions

------
twic
You can already do:

    
    
      def show((a, b, c)):
        print 'a = %s, b = %s, c = %s' % (a, b, c)
    

And there's no way to pass a, b, and c other than positionally. You can write:

    
    
      show((1, 2, 3))
    

But not:

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

~~~
rbonvall
That feature was removed in python 3:

    
    
        $ python2
        >>> def f((a, b)):
        ...   return a + b
        ...·
        >>> f((1, 2))
        3
    
        $ python3
        >>> def f((a, b)):
           File "<stdin>", line 1
            def f((a, b)):
                  ^
        SyntaxError: invalid syntax

