If you Ctrl+Click
the parameter name, it goes to the definition. How can we navigate to the variable declaration? If that’s not possible, does it aid in debugging? Should we rely on Ctrl+F
?
I prefer =variable
because it’s consistent with *args, **kwargs
. To me consistency with f-string is irrelevant because it’s an entirely different thing.
But either way I love the idea!
And I prefer variable=
because *args
and **kwargs
accept more than just variables, they accept expressions, whereas =variable
wouldn’t.
And talking about consistency with f-strings would be wrong anyway because they also accept expressions.
I was thinking about this for a bit and it sort of feels like this is a convoluted path to get back to positional arguments. Like, Python introduced the idea of keyword-only arguments and this idea is trying to go back.
If I’m writing a lot of code that looks like func(variable=variable, other_variable=other_variable, ...)
then maybe my function signature has bad ergonomics and I should make it possible to write func(variable, other_variable, ...)
. 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.
Just putting this idea out here as another perspective on the situation. I think it’s useful to consider how we find ourself in situations where sugar makes a big difference, and if there are ways to avoid needing it.
It depends a lot on the code you write, and the APIs you use in your field. Take something like scipy.optimize.minimize
as a random example from my currently open tabs. You will almost always use some of its arguments, but almost never all of its arguments. At least in my day to day work, I see a lot of easy wins by making it possible to write optimize(fun, x0, constraints=, tol=, options=)
.
This really comes down to what mental model of the abbreviation you want to champion:
=variable
says “here is an argument, go find its parameter”variable=
says “here is a parameter, go find its argument”
To me, the latter makes more sense, but I think both are valid points of view. [1]
In my mind, this also answers what IDEs should do when you go to definition. ↩︎
Agreed.
And importantly, we already have:
var = 1
print(var) # 1
print(f"{var=}") # var=1
reusing this to pass the keyword arguments shortly makes sense to me.
Thank you for sharing for this perspective. My view is that named arguments are a fantastic feature which are generally underutilised, I’d hope that this syntactic sugar is actually going to encourage their use. One main benefit of positional arguments is aesthetic. In my own experience, this is often a reason to use positional ones (I don’t want to turn a one-line function call into a four-line one).
In practice, this proposal is only ‘going back’ in the sense that the code aesthetically looks more like positional, but in practice, you still get all the great benefits of using named arguments.
My main reservation here is that this syntax encourages coupling the name of the variable in the caller with the name of the argument. This has two downsides for me - 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. To give an example that was quoted earlier, optimize(fun, x0, constraints=, tol=, options=)
is less clear to me than optimize(fun, x0, constraints=must_be_convex, tol=one_in_a_million, options=production_quality)
(excuse my lack of knowledge of what the function actually does )
Otherwise, I mostly agree with @jamestwebber that 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? (That’s not to say that every case is an example of bad API design, just that this feature might make it too easy for people to be a bit lazier about how they design their APIs).
I feel a little iffy about the fact that the callee signature will now exert an influence on what the caller names its local variables. Philosophically that seems backwards to me.
Then there’s the small issue of keyword invocation being slower than positional. Maybe the newest Pythons have done something to help this? I know in the past cattrs has gotten a measurable speedup by being careful about using positional args where possible. Probably a niche issue though.
As seen in the stats above, though, this already happens in huge numbers of places. Clearly it’s often the best choice even without this feature. And I’ve seen it time and again as I’m mentoring/tutoring: the name given for a parameter is frequently the best name for a variable that’s going to hold the value to be passed to it. It’s a way for the expertise of the library creator (including the Python stdlib, but also third-party libs) to guide an uncertain novice, and it’s frequently been beneficial - ensuring correct use of terminology, or correct spelling of a word, or anything like that.
IMO this is therefore actually a benefit.
For those interested, I drafted this PR illustrating the impact of applying this syntactic sugar across the board on cpython.
Impact:
- total instances of x=x keyword arguments: 4225 (11.06% of all keyword arg uses)
- lines saved (assuming max line length of 80): 290
A similar approach in pandas (where x=x
makes up 17.24% of all keyword arg uses) saves almost 1000 lines.
Caveat: this is purely illustrative and I’m not necessarily advocating for changing all instances of this pattern in this way
Obviously a very opinionated statement, but I’ll add it’s ugly and having to work with it in code would make me enjoy Python less.
Thanks for doing this. With the provisos that I only skimmed the PR, and I acknowledge that unfamiliarity always makes a new form look worse, I have to say that I found pretty much every change made in this PR looked terrible to me. I can’t honestly say why, but none of the changes felt like an improvement, and none of them even gave me the sense of “well, I guess I could live with it” that the constructed examples in this discussion left me with.
I think that PR pushed me from -0 to -1 on this proposal.
When mixed with positional and keyword args, it might be adding another layer of mental overhead by breaking the expected flow.
For instance a contrived example:
add_ingredient(
“milk”,
amount=calculated_amount,
temp=,
speed=5)
Thanks for producing this PR. It’s good to see this proposal applied on real code.
My impression is that it doesn’t aid in readability and often looks odd, more like a typo than anything else.
It removes some duplication, but these days editors make it rather easy to add such constructs, so the typing overhead is minimal.
I guess the impression would be different using a more imperative style of making the “copy the local var to this function/method call as a keyword parameter of the same name” operation obvious, similar to what *args
and **kwargs
do. Perhaps using %var
or &var
(the originally proposed =var
looks odd and also like a typo to me), with a preference for &var
, since this reads a bit like “and please add keyword variable var”.
In any case, I can well live without this feature
This has been my impression as well.
While I’ve used the construct occasionally in my own projects, I can’t think of an alternative that would solve my issue with that.
To me, var=var
is pretty explicit, conveys intention, and all the proposed syntax so far has been a step back from that.
While I agree that it’s an improvement over the current proposals, I feel like prior art would lead people to read it as passing a pointer to the function.
Obviously, this doesn’t make much sense in Python, but it’s what I think of when I see that syntax.
Agreed.
I’m not terribly opposed to it’s addition, but I probably wouldn’t use it in personal projects.
Sometimes I feel like we compare things to Rust too often, but there is a comparable feature in that language. Creating a struct is “keyword-only” unless you are passing in something with the same name as the field.
struct User {
name: String,
admin: bool,
}
...
let name = "James".to_string();
let a_user = User { name, admin: false };
I actually like this version, but it relies on the fact that Rust has much stricter rules.
I could imagine this for Python functions[1], but it is not backwards compatible because it reinterprets positional arguments as possibly-keywords-if-the-name-matches. That said, I suspect that in a large number of cases that would be affected, it’s a mistake: someone wrote f(bar)
but forgot that the signature is f(foo, bar)
and they intended f(bar=bar)
. Or they meant f(foo)
but they named the variable bar
because they forgot the signature (I do this in matplotlib all the time).
For anyone doing these big analyses of existing code, I’d be interested in knowing how often a positional argument is named the same thing as a different argument in the signature[2].
Yeah, that’d be quite hard to figure out - remember that *a,**kw will get in the way here.
Yeah, although I think we could just ignore uses of args and kwargs for this question.
It seems like it’s not possible just by filtering the text, but probably doable if you parse the code and so you know the signature at the time of calling (but that could be tricky).
I do think it’d be a valuable linter warning, at the very least. I run into this with some functions, for example because one argument’s default is always fine and I often need to change the next one, so I forget the first one exists as a positional arg.