|
|
Subscribe / Log in / New account

Implicit keyword arguments for Python

By Jake Edge
November 1, 2023

Python functions can use both positional and keyword arguments; the latter provide a certain level of documentation for an argument and its meaning, while allowing them to be given in any order in a call. But it is often the case that the name of the local variable to be passed is the same as the keyword, which can lead to overly repetitive argument lists, at least in some eyes. A recent proposal to shorten the syntax for calls with these duplicate names seems to be gaining some steam—a Python Enhancement Proposal (PEP) is forthcoming—though there are some who find it to be an unnecessary and unwelcome complication for the language.

Parameters and arguments

The parameter list of a Python function describes the names of its parameters; those parameters are matched up with the arguments that get passed in a call to the function. For example:

    def func(a, b, c=3, d=None):   # definition with four parameters
        pass

    func(1, b=42, c=9, d=6)        # a call with four arguments
But it is not infrequent that calls are made where the argument and parameter have the same name, which leads to some duplication in the call:
    func(a=a, b=b, c=c, d=d)
For parameters with short names, such as these, the duplication is probably not really significant, but longer parameter names change that picture:
    func2(visibility=visibility, frobnification_level=frobnification_level)
In those cases, the duplication just adds a lot extra noise, so Joshua Bambrick suggested adding some syntactic sugar to avoid the problem; his goal is to promote the use of keyword arguments, which he believes are underused in part because of this visual noise.

His original suggestion was as follows:

    # instead of this:
    func2(visibility=visibility, frobnification_level=frobnification_level)

    # this could be used:
    func2(=visibility, =frobnification_level)
He had a long list of benefits, relating several of them to entries in PEP 20 ("The Zen of Python"); the overall idea is to increase the readability while reducing verbosity. The change would be backward compatible, because the new construct is a syntax error in today's Python.

Nir Schulman thought that it made more sense to put the "=" at the end of the parameter name; it may be easier to implement that way and it is analogous to the f-string f'{var=}' construct.

    func2(visibility=, frobnification_level=)
Guido van Rossum agreed with that second syntax and was willing to be the core-developer sponsor of a PEP if someone wanted to write one. Bambrick and several others quickly volunteered to work on it.

There were several commenters who were in favor of the idea and the "postfix" version, with the = after the parameter name, proved to be more popular, so it seems likely to be the syntax proposed in the PEP. But "Miraculixx" strongly disagreed; it will confuse beginners, they said, who are already somewhat confused by keyword arguments. They had their own list of quarrels with the idea (with nods to PEP 20 as well), many of which come down to personal preference (as, of course, do the benefits Bambrick listed). But Miraculixx also pointed out that it could lead to a somewhat subtle and possibly hard-to-find error: "renaming the variable in the calling context will break the code and it is not obvious to fix".

Paul Moore agreed with those arguments, noting that he would not use the feature himself, nor allow it in projects he maintains. He said that he can live with it if the feature gets added, but: "Python does seem to be gaining features I'm preferring not to use much more than features I'm enthusiastic about, these days :("

Numbers

Chris Angelico, who was one of those who volunteered to help with the PEP, thought that some of Miraculixx's complaints were not entirely reasonable. Angelico also did some research on the prevalence of "var=var" in the Python standard library; he found 3858 examples, which suggests to him that there is a real need for the feature. Likewise, Hugo van Kemenade found many instances of the pattern in other Python code bases. Angelico said that the short "x=x" examples do not really do justice to the idea; his search found 525 places where the identifiers were ten or more characters in length (310 if the unit tests are eliminated):

And I would absolutely argue that there's clarity to be gained here:
    ElementTree(element).write(stream, encoding,
                               xml_declaration=xml_declaration,
                               default_namespace=default_namespace,
                               method=method,
                               short_empty_elements=short_empty_elements)

# vs #

    ElementTree(element).write(stream, encoding,
                               xml_declaration=,
                               default_namespace=,
                               method=,
                               short_empty_elements=)

He further argued that brevity, in and of itself, is not really the goal; instead, the feature will help people spot problems:

When xml_declaration= is the norm, it's obvious that xml_declaration=html_declaration must be intentional and cannot possibly be a transcription error (okay, that particular example probably wouldn't happen, but you get the idea).

Inevitably, some bikeshedding over naming took place. Tamás Hadházy asked: "what this syntactic sugar should be called?" Ideas ranged from "implicit named arguments" through "abbreviated keyword arguments" and "elided keyword arguments" to "shorthand-keyword-arguments" (and others). Bambrick noted that other languages call it "punning", though the reasons for that may be a little obscure. There was no real consensus on a name, though Bambrick did use "punning" in an example pull request (PR) with a massive diff that modifies the standard library to use the new syntax.

The conversation soon wandered back to the feature itself. James Webber wondered if the perceived need for it was actually due to "bad ergonomics" of various sorts in function definitions:

Maybe I've converted too many things to be keyword-only, or I've ordered my arguments in a silly way (like, put uncommonly-specified arguments too early). Syntactic sugar covers up the mess a little but the code itself could be improved.

Moore agreed; "this feels like it's helping to make functions with lots of keyword arguments more tolerable, but maybe there's an underlying problem where a better API design would avoid the need for lots of keyword arguments in the first place?" Beyond that, though, the feature encourages using the same variable name inside and outside the function definition, which has some downsides: "the first is that it makes refactoring (a little) harder, and the second is that it discourages using more meaningful, less generic names for the variables in the caller".

But Angelico noted that the statistics tell a different story: "[...] this already happens in huge numbers of places. Clearly it's often the best choice even without this feature." Bambrick's changes for the standard library, and a similar example PR for pandas seemed to harden the opposition to the feature somewhat. Several responses in the thread indicated that the changes did not improve readability, at least for most commenters, though there were also a few new messages in support.

Some other possibilities for the syntax were mooted, though no real consensus emerged. Like many others, it is a proposal with some strong advocates and roughly as many naysayers. The PEP should help further the discussion, but the feature may well be too niche—and too disruptive—to go much beyond that. As alluded to by Moore and others, there seems to be some concerted effort toward proposing fairly small changes at the corners of the language these days. It remains to be seen whether the steering council (or its delegate) is amenable to that.


Index entries for this article
PythonArguments
PythonEnhancements


to post comments

Implicit keyword arguments for Python

Posted Nov 1, 2023 18:21 UTC (Wed) by pwfxq (subscriber, #84695) [Link] (10 responses)

With modern IDEs which perform autocomplete and format as you type, is this feature really necessary?

Implicit keyword arguments for Python

Posted Nov 1, 2023 18:27 UTC (Wed) by mb (subscriber, #50428) [Link] (9 responses)

Yes, it is.
This improves readability. Which is a good thing.
It's important to make code readable, because code is read often.

Rust has a similar thing for instantiating structs. And it is really nice to use.

Implicit keyword arguments for Python

Posted Nov 1, 2023 20:01 UTC (Wed) by tialaramex (subscriber, #21167) [Link]

Yeah, reading Authorization { name, address, token, setting } tells me everything I'd have learned from Authorization { name: name, address: address, token: token, setting: setting } but more succinctly and in the process any special cases are highlighted.

Position { x: y, y: x, z: z } // Yeah yeah, tl;dr

Position { x: y, y: x, z } // jumps out as special, why are we... oh, x and y are swapped, that's probably important!

Implicit keyword arguments for Python

Posted Nov 2, 2023 13:31 UTC (Thu) by jhoblitt (subscriber, #77733) [Link] (7 responses)

IMHO, one of the ugliest things about rust is that it doesn't have named parameters.

Implicit keyword arguments for Python

Posted Nov 2, 2023 17:36 UTC (Thu) by mb (subscriber, #50428) [Link] (6 responses)

Yeah, well. I once thought that, too. But it actually is a minor issue.
Most of the time a mixup in the arguments will lead to a type mismatch and a build error in practice.
And there usually are not that many arguments to functions in Rust, because things are usually packed in structs or completely different concepts are used (like builders).

But you can have a named-parameter-ish thing in Rust, if you use a struct:

struct To {
addr: String,
port: u16,
}

fn connect(To { addr, port }) {
}

Implicit keyword arguments for Python

Posted Nov 2, 2023 18:06 UTC (Thu) by mbunkus (subscriber, #87248) [Link] (1 responses)

Apart from the compiler Rust also benefits from its expressive type system. You don't pass around a lot of arguments of the same type all the time (e.g. three integers where mixing them up is easy). Instead you often pass domain-specific types, which in combination with the compiler is really, really cutting down on accidentally switching them.

Implicit keyword arguments for Python

Posted Nov 3, 2023 11:56 UTC (Fri) by tialaramex (subscriber, #21167) [Link]

And this is even something which continued to improve after Rust 1.0

When they shipped Rust 1.0 there's no Duration type, so there are several standard library functions like blah_blah_ms which take a plain integer and that's how many milliseconds duration is meant. These APIs were deprecated in favour of replacements (in this case just blah_blah) that take a Duration, once it existed, which means we get this type protection, 1000 isn't a duration, so if you wrote blah_blah(10, Duration::ZERO) but you meant blah_blah(Duration::ZERO, 10) that won't compile as you said whereas blah_blah_ms(10, 0) and blah_blah_ms(0, 10) are equally valid and you may take a while to figure out why it's not working.

Implicit keyword arguments for Python

Posted Nov 2, 2023 18:41 UTC (Thu) by mb (subscriber, #50428) [Link] (3 responses)

Sorry, just realized that my brain was malfunctioning when I typed that code. Of course it would be something like:

fn connect(to: To) {}

connect(To { addr, port });

But I think you get the point.

That said, Python is a completely different story, due to the missing type system (for that purpose).

Implicit keyword arguments for Python

Posted Nov 3, 2023 8:02 UTC (Fri) by smurf (subscriber, #17840) [Link] (1 responses)

> That said, Python is a completely different story, due to the missing type system (for that purpose).

It's not missing. Just optional, with checkers like mypy (static) or typeguard (runtime).

Implicit keyword arguments for Python

Posted Nov 3, 2023 22:54 UTC (Fri) by himi (subscriber, #340) [Link]

Having started using type hints seriously in Python after getting reasonably comfortable using Rust (as well as learning some Haskell, and using OCaml enough to be comfortable with it in the past), I'm finding it an interesting mix. It's only really starting to work reasonably well for me since I started using pydantic, which makes it quite natural to treat Python classes more like Rust structs rather than classic objects - a repository of data that you do stuff with, rather than a collection of data and methods. Even without that massive shift in thinking, though, there are lots of hoops you have to jump through to get value out of type hints, which the language doesn't really accomodate well. And it's very much not the way I've used Python all these many years - it feels like a real step change, and for a while it made even /reading/ "modern" Python code difficult.

Using type hinting to approximate the kind of general type safety that you get with Rust seems to require completely changing the way you program in Python, even how you /think/ about programming in Python. To me that doesn't make it something that's there inherently, just turned off by default - it's a true add-on. Things are evolving pretty quickly, maybe after a while it'll become a more natural feature of the language, but it's nowhere near that point yet.

Implicit keyword arguments for Python

Posted Nov 3, 2023 9:37 UTC (Fri) by ojeda (subscriber, #143370) [Link]

Your original one, with a small change, also works:
fn connect(To { addr, port }: To) {
    // `addr` and `port` are directly available here, e.g.
    println!("{addr}:{port}");
}

Implicit keyword arguments for Python

Posted Nov 1, 2023 20:32 UTC (Wed) by NYKevin (subscriber, #129325) [Link] (1 responses)

Some use cases here:

* Write dict(foo=, bar=, ...) to easily take subsets of vars().
* Use arguments to __init__() to construct sub-objects.
* Call super().__init__() and pass arguments along.
* Pass arguments to logging functions (without using f-strings, so as to avoid the overhead of interpolating them when they won't be printed anyway).
* Pass along context variables and other opaque values.

Problem: Most of these should probably be using positional arguments in the first place. If the API forces keyword arguments, well, maybe the API is just wrong.

Exception to the problem: dict() has to be keyword-only (so that it knows the names of the variables). __init__() has to be keyword-only if using cooperative multiple inheritance (or else you have to be extremely careful about the MRO, which is usually Not Worth It).

In general, I am moderately leery of this syntax, but willing to be convinced. I've worked in a system that used dynamic scoping before, and while this is not quite as ridiculous as that, it still has the potential for pain if a variable is renamed or a codepath is refactored. OTOH, this is much more local and self-contained than true dynamic scoping, so it's less of a problem.

(To clarify what I mean by "dynamic scoping" - the language in question would let you define a variable in function A, call function B with no parameters, and then B can use the variable from A as if it was passed as a parameter to B - and this works recursively. If you call B from function C instead of function A, then it will go looking for the variable, find that it doesn't exist, and crash. Implicit keyword arguments are less bad, because in this scenario, you crash as soon as C tries to call B, instead of deep in the guts of B when it actually tries to use the variable, possibly multiple stack frames down.)

Implicit keyword arguments for Python

Posted Nov 1, 2023 22:09 UTC (Wed) by Kaligule (guest, #167650) [Link]

I am doing a lot of funtional programming in python. A lot of arguments are passed through functions unaltered and often they have long names.
It often feels too verbose.

I would use the heck out of this.

Implicit keyword arguments for Python

Posted Nov 2, 2023 0:04 UTC (Thu) by tbelaire (subscriber, #141140) [Link]

Yeah, I've reached for this before realizing it doesn't exist yet.

I am in favour , but I spend a fair amount of time in languages which already have it.

Implicit keyword arguments for Python

Posted Nov 2, 2023 5:28 UTC (Thu) by donald.buczek (subscriber, #112892) [Link] (2 responses)

What happend to "Explicit is better than implicit" ? "There should be one -- and preferably only one -- obvious way to do it." ?

Abbreviations reduce the space the code takes in your window so you get more overview and repeating constructs don't distract you from the important things.

On the other hand, you need knowledge of the increasing number of syntactical constructs. The code is more difficult to explain to a newcomer: See, _this_ is a parameter name of the called function, _this_ is a variable of the calling function, but _this_ is both and it uses a special syntax which can be used instead of the generic syntax for the cases only where both names happen to be the same.

I'm not opposed, but I see a disadvantage, too.

And I note that Python left its Zen long ago and became more and more perlish. TIMTOWTDI!

Implicit keyword arguments for Python

Posted Nov 2, 2023 10:11 UTC (Thu) by NRArnot (subscriber, #3033) [Link] (1 responses)

It is explicit. Implicit, would be a horrible proposal that if an argument is declared without a default in the function header but a variable of that name exists in the calling frame and nothing is passed by position, then that variable gets passed by default. (Note, horrible)

It's also better than the status quo because it makes it more explicit when one is doing something out of the ordinary

foo( z_pos = z_pos + dz,            # significant
       x_pos=,                      # boring
       y_pos=,
       ... ,
       ... 
)
I would slightly prefer that the designator wasn't '='. Maybe '~', or the currently unused '%'. Would make it easier to find this usage with an editor. But that's just arguing about the type of sugar in the coffee.

Implicit keyword arguments for Python

Posted Nov 2, 2023 12:42 UTC (Thu) by smurf (subscriber, #17840) [Link]

What's difficult about finding "=," with your editor? It's about as unlikely to occur anywhere else as "%," or "~," are.

Bare "%" and "~" are already valid Python operators, if not as prevalent as "=".

Implicit keyword arguments for Python

Posted Nov 2, 2023 7:54 UTC (Thu) by bushdave (guest, #58418) [Link] (2 responses)

I have craved this feature and have also got stuck on what a good syntax would be. The two main languages I work with are Python and TypeScript, and TS has had this feature with objects (which is what you use for keyword arguments in that language). The feature is called Object Property Value Shorthand there, as well as in JavaScript from which it is inherited.

The need for this arose when I decided that, with a few exceptions, functions should not have more than one positional argument. Very few functions have self-evident ordering of the arguments, and thus they should be explicit.

Would it be horrible to re-use a reserved word for this? I'm thinking of using pass to mean "pass on the value of the local variable as a named parameter of the same name":

def do_the_frobnicate(frobnication_method):
  frobnicate(frobnication_method=pass)

Implicit keyword arguments for Python

Posted Nov 9, 2023 20:48 UTC (Thu) by NAR (subscriber, #1313) [Link]

functions should not have more than one positional argument. Very few functions have self-evident ordering of the arguments, and thus they should be explicit.

If the ordering is not self-evident, chances are that the naming of the function parameters are not self-evident either, so I need to look up the function definition anyway (which might be a keystroke or a mouse movement away in the IDE, so no big deal).

Implicit keyword arguments for Python

Posted Nov 11, 2023 10:39 UTC (Sat) by sammythesnake (guest, #17693) [Link]

I was thinking it'd be quite helpful to be able to use something like `with` e.g.

foo(bar, baz=flib) with (glub, twinkle)

being equivalent to

foo(bet, baz=flib, glub=glub, twinkle=twinkle)

I've often pondered a similar syntax for closure type constructs to define variables in the calling scope you're happy for the called function to see, which could reduce a mountain of general contextual parameters where there's a lot of delegation to other functions. A pattern I've struggled to avoid ergonomically myself is using some form of "context" parameter (the naïve version being a dict) that can be tweaked and passed forward while leaving uninterestingly unadjusted fields unmentioned. That leaves me needing a lot of repetitive boilerplate to check invariants which soon means defining a class hierarchy just for the parameter lists of a family of related functions.

The signal/noise ratio of this kind of code is really poor, so I'm generally in favour of ways to express this kind of mechanical stuff concisely, providing the syntax is reasonably guessable or at least googleble. The relative googlability of keywords vs. punctuation is something rarely given as much weight in language design as I'd like. Julia's REPL does a great job of providing help with this - unless you need to look up the LaTeX code for, say, "ε” ("\varepsilon") that is...

Confusing syntax

Posted Nov 2, 2023 8:23 UTC (Thu) by rrolls (subscriber, #151126) [Link] (8 responses)

I just don't like the postfix version ( func(arg=) ) that's been recommended, at all. I really hope this doesn't get added to the language.

I wouldn't mind seeing the prefix version ( func(=arg) ), but the best solution is probably to do nothing.

My reasoning:

You read and write from left to right.

The prefix version makes it clear straight-up "this arg is being passed from a local!" and then you get to read the name of the arg.

The postfix version on the other hand looks like unfinished code: when reading it, you just think "did I start adding a new argument here and then get distracted? I better finish that... oh, it's just supposed to be an implicit arg, right." Alternatively, when writing, suppose you _did_ distracted after writing the '=', and never did come back to finish it. If that arg happens to exist as a local and doesn't cause an obvious immediate error, you're boned.

Personally, whenever a func needs more than about six arguments (or more than about TWO optional arguments) I just stick them all in a dataclass (I've made myself a convention of naming this kind of dataclass something ending "Params"). That solves the problem for me: I can now just pass that instance around, instead of a bunch of arguments. Hence why I don't think this is really needed at all, and do-nothing is the best solution here.

Confusing syntax

Posted Nov 2, 2023 18:04 UTC (Thu) by mbunkus (subscriber, #87248) [Link] (1 responses)

I've always felt like the Python devs violate the "reading follows the same order as execution" principle almost intentionally, given things like

some_var = [ frobnicate(important_var) for important_var in range(1, 10) if testify(important_var) ]

I'm a bit hyperbolic, of course. Still, I find this type of syntax to be… not better than Perl.

Confusing syntax

Posted Nov 3, 2023 20:07 UTC (Fri) by NYKevin (subscriber, #129325) [Link]

I'm pretty sure comprehensions are copying set-builder notation.[1] You could blame the mathematicians for that one, but then math doesn't even have an execution order in the first place. From their perspective, the most important thing is the actual element of the set, which comes first, and then you add a bunch of predicates after that to restrict it. Then, when you want to turn it into a programmatic concept, you need the whole expression to range over a finite domain (infinite sets are hard to model programmatically, but some languages will at least attempt it). A domain is conventionally placed right after the element expression with ∈ (pronounced "in"), so that domain restriction becomes the "for x in y" part.

[1]: https://en.wikipedia.org/wiki/Set-builder_notation#Sets_d...

Confusing syntax

Posted Nov 3, 2023 8:39 UTC (Fri) by Kaligule (guest, #167650) [Link] (4 responses)

I am torn between the prefix and suffix: I normally write these arguments from top to bottom:

relevant_events = find_relevant_events(
start_datetime=,
end_datetime=,
place=,
observer=,
)

If the arguments are of different lengths it is a bit hard to find the equal sign at the end. If it was at the front they would almost act like a bullet point.

Confusing syntax

Posted Nov 3, 2023 8:48 UTC (Fri) by smurf (subscriber, #17840) [Link]

> If the arguments are of different lengths it is a bit hard to find the equal sign at the end.

On the other hand, you only need to find the first one. The others are required.

Confusing syntax

Posted Nov 3, 2023 22:30 UTC (Fri) by Kamilion (guest, #42576) [Link] (2 responses)

yeah, this is definitely something where I'd prefer a prefix; actually, the pipe operator would "look good" here.

Much moreso than an equals suffix followed by a comma...

relevant_events = find_relevant_events(
|start_datetime,
|end_datetime,
|place,
|observer,
)

That just looks so much more visually followable, with or without the appropriate whitespace formatting.
If I saw this without knowing how the feature worked, I could *easily* guess at what it was doing if I saw those other identifiers defined/used nearby. That's at least at lot closer to the principal of least surprise, for me.

Confusing syntax

Posted Nov 3, 2023 22:37 UTC (Fri) by Kamilion (guest, #42576) [Link] (1 responses)

Grabbing the example from earlier in the thread...

Position { x: y, y: x, z }

->

Position { |y, |x, |z }

"oh, that must pipe those variables into those positions, and X and Y have been swapped."

Why would I find that useful?

D3DSwizzle { |b, |g, |r, |a }

OGLSwizzle { |y, |x, |z, |w }

It just "feels" more pythonic to me somehow. Dunno. *shrug*

Confusing syntax

Posted Nov 4, 2023 8:50 UTC (Sat) by smurf (subscriber, #17840) [Link]

> Position { x: y, y: x, z } -> Position { |y, |x, |z }

These two are not equivalent. The second version would translate to x=x,y=y,z=z.

Confusing syntax

Posted Nov 7, 2023 6:06 UTC (Tue) by mirabilos (subscriber, #84359) [Link]

That was my first thought on reading about this as well, dread growing soon…

Implicit keyword arguments for Python

Posted Nov 3, 2023 23:12 UTC (Fri) by smitty_one_each (subscriber, #28989) [Link]

I'm in favor. I already use

print(f'some argument {value=}')

extensively, so this seems both a logical extension and an argument against =value.

Implicit keyword arguments for Python

Posted Nov 9, 2023 10:54 UTC (Thu) by callegar (guest, #16148) [Link] (1 responses)

Will this also work for the frequent `self.somevariable = somevariable` in the `__init__` body?

Implicit keyword arguments for Python

Posted Nov 9, 2023 12:41 UTC (Thu) by smurf (subscriber, #17840) [Link]

Nope. That's a different problem. it's also basically solved, via the "attrs" or "dataclasses" module.


Copyright © 2023, 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