I believe this contradicts the very essence of a function definition. A function is meant to be a self-contained unit, a distinct part of a larger whole. Allowing variable names as keywords would interlink function definitions, making them dependent on each other and essentially forcing them to behave as a single entity. What term would accurately describe this concept? Co-joined functions?
The functions would remain self-contained units. This syntax improvement is only meant to operate at the calling level and doesn’t affect the function definitions themselves. You can think of it as a built-in macro. That is, .argument_name
would be automatically expanded to argument_name=argument_name
at the time of function invocation, and hopefully a variable with the same name exists in the scope. This is similar to how many other macros work.
It’s already very common for the same purpose of variable to be given the same name in an entire family of functions. Perhaps the strongest example of this is that the first argument to nearly every method ever written is self
, although since it’s passed positionally, that won’t really help here.
But let’s have a quick look at the subprocess module. There is the fundamental Popen
constructor, which accepts a wide variety of keyword arguments, and then there are four other functions (run()
, and three older functions call
, check_call
, and check_output
) which pass keyword arguments on to Popen. For the most part, they use **kwargs
to pass those on, but call, run, and check_output all have explicit timeout
parameters, and they all use the timeout in broadly the same way - passing it along to an underlying function as a keyword argument, timeout=timeout
. As a programmer, you would expect that all of these timeouts mean the same thing - and they do.
Yes, technically the timeout
parameter on run() is independent of the one on check_output(), but they also aren’t unrelated.
So I’ve posted a duplicate of this and hence would like to reiterate some of the points I discuss there.
I think this suggestion put together two separate needs from the language - the need for a name-value pair that can be used as such semantically; and the ability to get the variable’s name as a string in code.
Overlap with f-string debug specifier: In this sense this does tie this suggestion with the semantics for the f-string debug specifier. The repetitions found in code such as in the pickle library (as a random example):
return _Unpickler(file, fix_imports=fix_imports, buffers=buffers,
encoding=encoding, errors=errors).load()
are there because the variables and the function parameters share semantic meaning (to respond to Elis’ concern). A similar motivation is why we want the f-string debug specifier. And why should we stop there? We would like to be able to use %
formatting with name-value pairs too - specifically for logging (specifically the scenario of lazy formatting)!
Scope of variable name: It is a question - whether the name of the variable should carry significance outside its scope. I argue it’s the case anyhow in some many places, and it’d definitely be better if this is somehow indicated declaratively. It makes the code easier to read, less verbose, and facilitate refactoring. It will beckon a person doing code changes to any keyword arguments – do they still serve the same semantic purpose the call site assumes they do, by either forcing them to decouple the names or rename callees while they’re at it.
Other languages: As mentioned in other places, this facilitates a construct similar to JavaScript’s shorthand for object initialization, where {a, b, c}
would be similar to dict(a=, b=, c=)
per this suggestion (I also dislike the star syntax). I don’t know of any other language that does this, but this is an awesome, widely used feature of Javascript.
Use in Python codebase: Using a simple grep expression I found ~2000 lines of codes that would benefit from this. Probably there is more. (egrep --include '*.py' -o -r '[, ([]([a-z0-9_]+) *= *([^.]+[.]|)\1,' cpython | wc -l
)
At odds with f-string debug specifier: In my original post, I suggested that the expression a=
create a thin name-value object that would be interpretable in place of a kwarg or in an f-string. However, I do argue that a “short name” be used as the qualifying name here, rather than a long name, e.g. a.b=
should produce the name b
rather than a.b
(especially relevant for instance variables, e.g. self.b
). This is dissimilar to the way f-string debug specifier is designed to work, so possibly one can think of this name-value object as being formatted with the full expression string, but passed as a kwarg with the “short name” (if possible to resolve one, otherwise erring at parse time).
Some counter-arguments to counter-arguments:
- Too easy to overlook: concise code tends to have people looking in the right place rather than the wrong one. Inconsistencies between lhs and rhs kwargs are much easier to overlook. Any mistakes will quickly be highlighted by any static check. Plus, it’s just as easy to overlook in f-strings.
- Not knowing what it means if you don’t already know: the same can truly be said to any symbol, namely
=
it self (is it an assignment operator or an equality one?) Any symbol operators introduced to a language suffer from this disadvantage. The fact it, though, that is does not add a symbol, rather it shortens existing expressions, asdict(a=a)
becomesdict(a=)
. It could arguably bedict(a=!)
,dict(a=↫)
ordict(a==)
but this doesn’t really matter.
Finally, does it make Python better? I would say yes:
- It’s practically built-in to the language (as kwargs are dicts anyway).
- It allows one to handle a variable’s name as if it was code, which it is.
- It gives meaning to the notion of the name-value pair and may facilitate more advanced uses in the future.
- Bringing together two seemingly disparate parts of the language together - kwargs and f-string debug specifiers - will make it simpler. It also facilitate re-use of the construct in other places (e.g. lazy formatting as in logging)
- It reduces greatly the amount of code repetition, resulting in fewer possible errors, and more readable code.
One alternative way to do this could be to make creation of string-name of variable to variable dictionaries easier. Then, you can simply unpack this dictionary using **kwargs
. Current way is kind of clunky:
import inspect
def retrieve_var_names(*args):
callers_local_vars = inspect.currentframe().f_back.f_locals.items()
return {var_name: var_val for var_name, var_val in callers_local_vars if var_val in args}
kwargs = retrieve_var_names(weight_decay, momentum, lr, dampening, nesterov, has_sparse_grad, maximize)
func(params, d_p_list, **kwargs)
Similar to the way we have dict.from_keys
, some similar constructor can be made for this pattern (dict.from_value_names
?), or it may be shelved somewhere in itertools
.
A function’s variable name is not (and should not be) visible in another function.
Users don’t have to use the same parameter names as those used in the pickle implementation code (or other standard libraries).
Is this a significant concern in real-world usage, or is it primarily relevant to the mentioned libraries in this thread, e.g., pickle? In other words, you can define a class that inherits from the Unpickler
class and use positional parameters instead.
I typically define a ‘settings’ key-value variable to store all the required keyword parameters and pass them as **kwargs
when necessary. That’s why I use keyword parameters in my class definitions.
@elis.byberi What I’ve suggested is not that you make variable names visible to other functions. It’s that you can pass on explicitly copying code when calling other functions. That’s all – no more than that. What I’m suggesting is not more “permissive” than passing on **kwargs
which is totally acceptable and used all around (namely decorators and wrappers). I would go as far as to say that whenever a language offers keyword arguments, it should offer this feature.
Yes, it has wide-spread real-world usage. In the Python library itself I found over 4,000 non-singular uses (meaning the library didn’t use this style of assignment only once, but at least 4 times). About half were under Lib/test
. But that leaves the other half elsewhere. Here’s a nice plot. I split them into var
assignments (a-la dict(a=a)
) and attr
assignments (a-la dict(a=x.a)
).
(I used AST to so this. You can also play around with the code I used to make this)
It’s also very, very widespread in Javascript, which is a very widespread language on its own.
This seems to be a very common idea to make python better, as I suggested something essentially the same here: Syntactic sugar to encourage use of named arguments
Only minor caveat was that I propose making the syntax more consistent with python’s long-standing *args
and **kwargs
syntax (established pythonic style), i.e.
my_function(=my_first_variable, =my_second_variable, =my_third_variable)
I listed the benefits as:
- Encourages use of named variables, thereby increasing readability (explicit is better than implicit) and reducing bugs from argument transposition
- Reduces verbosity (readability counts)
- Encourages authors to use the same variable name when calling a function as the argument (increases consistency of variable names used, thereby increasing readability)
- Reminiscent of python’s long-standing
*args
and**kwargs
syntax (established pythonic style) - Reminiscent of python’s existing f-string debug
f'{var=}'
syntax (established pythonic style) and with a very similar function - This should be relatively easy to implement as it is simple syntactic sugar (if the implementation is easy to explain, it may be a good idea)
- Backwards compatibility: this change is fully backwards compatible since the proposed syntax would raise a syntax error in current python versions
I think some confusion may be caused by the OP’s suggestion to automatically expand arguments without any change in syntax. I’d oppose that for the reasons as listed, but I think these issues are entirely ameliorated by the proposed =
syntax:
- the changes are entirely local at the call site
f(=a)
is immediately expanded intof(a=a)
- there is no confusion about how to match variables to arguments (at least, none more than existing function call syntax)
- it does not require looking at the function called
The problem with the form =a
is that it might not be clear that what comes after the =
should be only a name, so =a
is OK but =a+b
isn’t. The form a=
doesn’t have this problem.
In the cases of *args
and **kwargs
at the call site, args
and kwargs
are not limited to only names.
That might not have to be a problem, for the same reason as…
… this. My guess is that it would usually not be useful, though, so while I personally think it would be simpler to permit it (and yes, you would have a kwarg named “a+b”), I wouldn’t fight too hard for this use-case.