Implicit keyword arguments for Python
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 argumentsBut 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 | |
---|---|
Python | Arguments |
Python | Enhancements |
Posted Nov 1, 2023 18:21 UTC (Wed)
by pwfxq (subscriber, #84695)
[Link] (10 responses)
Posted Nov 1, 2023 18:27 UTC (Wed)
by mb (subscriber, #50428)
[Link] (9 responses)
Rust has a similar thing for instantiating structs. And it is really nice to use.
Posted Nov 1, 2023 20:01 UTC (Wed)
by tialaramex (subscriber, #21167)
[Link]
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!
Posted Nov 2, 2023 13:31 UTC (Thu)
by jhoblitt (subscriber, #77733)
[Link] (7 responses)
Posted Nov 2, 2023 17:36 UTC (Thu)
by mb (subscriber, #50428)
[Link] (6 responses)
But you can have a named-parameter-ish thing in Rust, if you use a struct:
struct To {
fn connect(To { addr, port }) {
Posted Nov 2, 2023 18:06 UTC (Thu)
by mbunkus (subscriber, #87248)
[Link] (1 responses)
Posted Nov 3, 2023 11:56 UTC (Fri)
by tialaramex (subscriber, #21167)
[Link]
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.
Posted Nov 2, 2023 18:41 UTC (Thu)
by mb (subscriber, #50428)
[Link] (3 responses)
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).
Posted Nov 3, 2023 8:02 UTC (Fri)
by smurf (subscriber, #17840)
[Link] (1 responses)
It's not missing. Just optional, with checkers like mypy (static) or typeguard (runtime).
Posted Nov 3, 2023 22:54 UTC (Fri)
by himi (subscriber, #340)
[Link]
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.
Posted Nov 3, 2023 9:37 UTC (Fri)
by ojeda (subscriber, #143370)
[Link]
Posted Nov 1, 2023 20:32 UTC (Wed)
by NYKevin (subscriber, #129325)
[Link] (1 responses)
* Write dict(foo=, bar=, ...) to easily take subsets of vars().
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.)
Posted Nov 1, 2023 22:09 UTC (Wed)
by Kaligule (guest, #167650)
[Link]
I would use the heck out of this.
Posted Nov 2, 2023 0:04 UTC (Thu)
by tbelaire (subscriber, #141140)
[Link]
I am in favour , but I spend a fair amount of time in languages which already have it.
Posted Nov 2, 2023 5:28 UTC (Thu)
by donald.buczek (subscriber, #112892)
[Link] (2 responses)
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!
Posted Nov 2, 2023 10:11 UTC (Thu)
by NRArnot (subscriber, #3033)
[Link] (1 responses)
It's also better than the status quo because it makes it more explicit when one is doing something out of the ordinary
Posted Nov 2, 2023 12:42 UTC (Thu)
by smurf (subscriber, #17840)
[Link]
Bare "%" and "~" are already valid Python operators, if not as prevalent as "=".
Posted Nov 2, 2023 7:54 UTC (Thu)
by bushdave (guest, #58418)
[Link] (2 responses)
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
Posted Nov 9, 2023 20:48 UTC (Thu)
by NAR (subscriber, #1313)
[Link]
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).
Posted Nov 11, 2023 10:39 UTC (Sat)
by sammythesnake (guest, #17693)
[Link]
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...
Posted Nov 2, 2023 8:23 UTC (Thu)
by rrolls (subscriber, #151126)
[Link] (8 responses)
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.
Posted Nov 2, 2023 18:04 UTC (Thu)
by mbunkus (subscriber, #87248)
[Link] (1 responses)
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.
Posted Nov 3, 2023 20:07 UTC (Fri)
by NYKevin (subscriber, #129325)
[Link]
[1]: https://en.wikipedia.org/wiki/Set-builder_notation#Sets_d...
Posted Nov 3, 2023 8:39 UTC (Fri)
by Kaligule (guest, #167650)
[Link] (4 responses)
relevant_events = find_relevant_events(
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.
Posted Nov 3, 2023 8:48 UTC (Fri)
by smurf (subscriber, #17840)
[Link]
On the other hand, you only need to find the first one. The others are required.
Posted Nov 3, 2023 22:30 UTC (Fri)
by Kamilion (guest, #42576)
[Link] (2 responses)
Much moreso than an equals suffix followed by a comma...
relevant_events = find_relevant_events(
That just looks so much more visually followable, with or without the appropriate whitespace formatting.
Posted Nov 3, 2023 22:37 UTC (Fri)
by Kamilion (guest, #42576)
[Link] (1 responses)
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*
Posted Nov 4, 2023 8:50 UTC (Sat)
by smurf (subscriber, #17840)
[Link]
These two are not equivalent. The second version would translate to x=x,y=y,z=z.
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…
Posted Nov 3, 2023 23:12 UTC (Fri)
by smitty_one_each (subscriber, #28989)
[Link]
print(f'some argument {value=}')
extensively, so this seems both a logical extension and an argument against =value.
Posted Nov 9, 2023 10:54 UTC (Thu)
by callegar (guest, #16148)
[Link] (1 responses)
Posted Nov 9, 2023 12:41 UTC (Thu)
by smurf (subscriber, #17840)
[Link]
Implicit keyword arguments for Python
Implicit keyword arguments for Python
This improves readability. Which is a good thing.
It's important to make code readable, because code is read often.
Implicit keyword arguments for Python
Implicit keyword arguments for Python
Implicit keyword arguments for Python
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).
addr: String,
port: u16,
}
}
Implicit keyword arguments for Python
Implicit keyword arguments for Python
Implicit keyword arguments for Python
Implicit keyword arguments for Python
Implicit keyword arguments for Python
Your original one, with a small change, also works:
Implicit keyword arguments for Python
fn connect(To { addr, port }: To) {
// `addr` and `port` are directly available here, e.g.
println!("{addr}:{port}");
}
Implicit keyword arguments for Python
* 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.
Implicit keyword arguments for Python
It often feels too verbose.
Implicit keyword arguments for Python
Implicit keyword arguments for Python
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)
Implicit keyword arguments for Python
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
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.
Implicit keyword arguments for Python
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)
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.
Implicit keyword arguments for Python
Implicit keyword arguments for Python
Confusing syntax
Confusing syntax
Confusing syntax
Confusing syntax
start_datetime=,
end_datetime=,
place=,
observer=,
)
Confusing syntax
Confusing syntax
|start_datetime,
|end_datetime,
|place,
|observer,
)
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
Confusing syntax
Confusing syntax
Implicit keyword arguments for Python
Implicit keyword arguments for Python
Implicit keyword arguments for Python