Syntactic sugar to encourage use of named arguments

Ok re: my IDE thing: The current behavior in my IDE (pycharm) is that if I have

foo(x=x)
foo(x=x,)

If I put my cursor anywhere after the = (but before the closing , or )) and hit the jump-to-declaration hotkey it jumps to the definition of the variable x. This behavior can be preserved after PEP 736.

1 Like

One upside that I don’t see explicitly mentioned: This will be really nice for those rare cases where parameters (and the variables that will be passed into them) warrant long names to be sufficiently descriptive. Currently if you have really long parameter + variable names you run the risk of exceeding the project line length limit. Of course this depends a little bit on the line limit chosen and how choices are made about multi-function call whitespace.

Some people might see this as a downside if it makes people more comfortable with overly long names.

Instead of debating the semantics of “implicit”, which itself is an appeal to Zen, how about answering the question “is this level of abstraction a boon or a blight” and leave Zen out of it?

It seems established that the best case for 736 is client code calling an api with named args (where the inputs are true locals and not named args from the containing function), rather than the “passthrough” args in e.g numpy as argued in the PEP. Kwargs feels a more appropriate fit for the latter (it’s clearly more maintainable when adding new args to deeply-nested calls, as are in numpy, matplotlib, etc). To be a little sassy, Zen also says “Namespaces are one honking great idea”!

1 Like

types.SimpleNamespace is close:

from types import SimpleNamespace
kwargs = SimpleNamespace()
kwargs.param_1, kwargs.param_2 = resolve_param_1_and_2("foo", 42, 1.23)
...
foo(**kwargs.__dict__)

Yes, the need to reference __dict__ is annoying, and a bit ugly.

You can also write your own pretty trivially:

>>> class NS:
...   @property
...   def _(self):
...     return self.__dict__
...
>>> n = NS()
>>> n.a = 1
>>> n._
{'a': 1}

I chose _ for the “get me the dictionary” property to avoid clashing with any attributes you want to add. But you can make your own variation. For example, by defining __call__ you could have n() give you the kwargs dictionary.

Of course, this isn’t in the stdlib - but conversely, something that was in the stdlib would need to be over-engineered to cover every use case, whereas something you write yourself can literally be just a few lines.

As has been said before, not every 3-line function (or class) needs to be in the stdlib.

Having said all of this, it’s very much a digression from the question of whether PEP 736 is a good idea. I still dislike it for all the reasons I’ve already stated, even though I don’t think “look, you can rewrite things using **kwargs” is a convincing argument against the proposal…

5 Likes

func(**vars(ns)) is more pleasant. I also tossed this out, which is effectively the same, but for some reason the SimpleNamespace folks don’t really like it :sweat_smile:

We are only debating what “implicit” means since that is the stick most often used to beat PEP 736. Some commenters are saying “I do not like PEP 736 because the variable name is implicit”. This has been addressed in the PEP, but apparently not to the full satisfaction of everyone. And hence we try to clarify this point. For example, what I was trying to point out is that the variable name is explicitly the same as the keyword name, and hence not implicit at all.

If you leave the Zen out of it, which I think you should, then what exactly is the criticism? “Is this level of abstraction […]” does not make sense to me, because there is no abstraction here in the first place. PEP 736 merely proposes a short-hand notation where f(x=) means the same as f(x=x), which is about as concrete as anything can be.

1 Like

That was regarding Chris’s sentiment that implicit roughly means abstraction, and it’s abstractions all the way down (which is not a bad thing). I was trying to choose less binary wording.

Restating my reservation b.c. megathread, I worry that 736 pulls hard towards named arguments (by design), and so it also pulls away from kwargs, which appears to me to be the “better” design in many cases. I like PEP 692’s approach for allowing “nice” function signatures including kwargs, but 736 and 692 are mutually exclusive.

Yeah in retrospect SimpleNamespace is closer, the initial comment was based on it not being a Mapping and thus doesn’t support unpacking with **.

Sometimes I forget how flexible Python is…

class MappingNamespace(dict):
    __setattr__ = dict.__setitem__
    __getattr__ = dict.__getitem__

n = MappingNamespace(a=1, b='bar')
n.c = "baz"
foo(**n)

Not saying I endorse this code, but it does the thing. Anyway I’ll stop this digression and let people go back to arguing about definitions.

3 Likes

Restating my earlier example (which is real code I wrote that day), consider that I am plotting something with matplotlib, and this is the code that I am writing currently in my script or Jupyter notebook:

    cbar = fig.colorbar(
        ...
        ax=ax,
        location=location,
        fraction=fraction,
        pad=pad,
        shrink=shrink,
        aspect=aspect,
        label=label,
    )

PEP 736 allows this to be deduplicated:

    cbar = fig.colorbar(
        ...
        ax=,
        location=,
        fraction=,
        pad=,
        shrink=,
        aspect=,
        label=,
    )

As I understand it, this is more or less the entire use case for PEP 736. What is the relation to PEP 692?

3 Likes

Would something like this be considered explicit enough?

f(a, b, c, pass x, y, z)

For me, the criticism is:

  • The syntax couples the name as used in the function signature to the name of a local variable in the caller in a way that can’t be controlled by the user (in the sense that if you use this construct the names must match). I understand that’s by design, but it’s a design that assumes that “not having to write the name twice” is more important than “being able to use a local name of your choice”. I think that’s a bad trade-off and the more general existing construct is sufficient[1]
  • To put it another way, the proposal’s claimed benefit is that you can omit the argument when it’s “obvious” what it is. I disagree that omitting the argument is a sufficient benefit, and I also don’t think that the construct will be common enough that a small benefit will scale sufficiently to be an overall improvement.
  • Also, the transition if you rename a local variable is unnatural and error-prone. Rather than simply renaming the variable in all the places it occurs, you have to also add the variable in a place where it’s currently omitted (and locating those places, while not hard, is not trivial either).

  1. sorry if this is worded a bit badly, I was struggling to avoid using the word “explicit” so that I wouldn’t get accused of invoking the Zen… ↩︎

7 Likes

Where are those variables defined? Here are my arguments depending on the answer:

  1. If they’re locals used once, why not pass the literals directly (save seven lines)?
  2. If they’re locals used multiple times, why not use a dict and ** (just pass the one namespace to multiple functions, not seven named args to each)? Even if they require processing (they aren’t simply holding literals), putting them in a namespace and referring to them as attributes isn’t exactly bending over backwards IMO.
  3. If they’re arguments to a containing function you wrote, why not refactor as **kwargs and use PEP 692 to declare which names are expected by kwargs (if you’re mindful of developer quality of life, IDE suggestions, etc, but if not then don’t)? Using kwargs lets you pass-through additional arguments without changes (seems like a significant plus to me, and this is the pattern I use all the time with windowing/gui/plotting libraries that require a lot of config).
  4. If they’re arguments to a containing function where you have no control over the signature, then, sure use 736, but this feels like a “bandaid” just because **kwargs was excluded and the signature was set in stone. Chances are the function was already written, so not worth the “drive-by” refactor to use 736. This alone dosen’t seem worth the risk of providing incentives away from kwargs when kwargs seems to be the “best” solution in the other cases.
  5. Did I miss one? Or more?
1 Like

Restating my issue with this example (and others like it), this feels like bad API design in matplotlib. It’s understandable (designs like this can “just grow” over the lifetime of a long-running and popular project, and redesigning them can be impossible for compatibility reasons) but I’d be happier if the proposal was motivated by APIs that were more clearly “good practice”.

Basically, a language’s capabilities influence the design of APIs in the language (look at the popularity of “fluent” APIs in Javascript for an example of this) and it’s not clear to me that this proposal is going to encourage better API design[1]. In fact, it feels to me that it’s more likely to encourage APIs with lots of generically named arguments, which is exactly the sort of design I’d argue against.


  1. You can argue forever over what a “good API” looks like, and I want to avoid using the term “Pythonic” to simply mean “whatever I like”. But IMO it is possible, if you take an unbiased look, to distinguish APIs that fit “naturally” in Python from ones that feel like they are awkward, or imported from other languages. Look at the stdlib logging and unittest APIs for a examples of designs that are clearly imported unchanged from Java… ↩︎

7 Likes

I’ll defend matplotlib because it uses **kwargs, and specifically NOT a wall of named arguments. My only criticism is that it’s not developer-friendly because the expected kwargs are buried in documentation, but PEP 692 solves that problem.

2 Likes

I think that problem of “coupling” has been voiced a few times, and I agree this is fair criticism. The PEP should reflect it carefully. I would respond that we see in the real world that often (and people have quantified how often) the name of a local variable is the same as the keyword of a function, purely because that is a good name for the thing at hand. This happens today, without PEP 736. And in that case, where it already makes sense for a local variable to have the same name as a function parameter, I would consider PEP 736 to exploit such “convergence”, instead of enforcing any sort of “coupling”.

Again, I take the point that people may start calling local variables the same as function parameters just to apply PEP 736, even when that does not make sense. To which I can only say: maybe don’t do that? I would certainly flag it in a code review, same as today.

1 Like

Well, no, that isn’t what I said.

I’m not sure this syntax change will actually achieve what it sets out to do. Assuming it makes it into 3.13, only leaf packages which target 3.13 and higher will be able to use the new syntax (as older versions will straight-up reject the code, as it can’t effectively be put behind a conditional). Packages which for now need to maintain support for older versions (see e.g. NEP 29 — Recommend Python and NumPy version support as a community policy standard — NumPy Enhancement Proposals, which has a 2 year/42 months support cycle) won’t be able to adopt it, nor any of their dependencies (and so on down the dependency tree). This change also targets the caller, not the callee, when the move to using keyword arguments needs to be done on the callee side (as if the callee does not expect users to use keyword arguments, then they may change the name used in the function definition, causing the caller code to break).

Perhaps a better way (assuming the intent of this PEP was for encouraging the use of keyword arguments) would be to introduce a helper decorator which allowed callees to raise a warning about a future switch to requiring keyword arguments, and which was written in such a way that the various linters and static analysis tools could easily detect and warn their users (and perform fixups if that’s a feature they had). This would require no syntax changes, so would keep working on older versions of Python (meaning it could be adopted by trunk projects much faster).

Minor aside: as mentioned by others above, it may be worth doing a update of the PEP, and creating a thread in PEPs - Discussions on Python.org, as I think there’s quite a few people who mute Ideas - Discussions on Python.org.

1 Like

By default, parameters are positional-or-keyword. So the caller CAN switch to keyword arguments without any change on the library’s end. Since this has always been possible, renaming arguments in this way would already cause potential breakage, so I don’t see how this proposal makes this any worse.

1 Like

Completely independent of my thoughts on this proposal, this is a weird statement. By design, and the stated goal of this PEP, this encourages the use of keyword arguments, meaning the goal is for more people to use them. This means (if the PEP succeeds) name coupling becomes a larger “problem” and renaming parameters is more likely to cause problems.

It also additional means that adopting to parameter name changes is a slightly larger patch, although I don’t find that all that relevant to consider.

1 Like

Well, yes. This is a change which can not pay off for the ecosystem till ~3.18. Thats how PEPs, especially syntax changes, work. They are lomg term goals. Short term, something like the decorator you suggest might work, but even that has a timescale of ~2-3 years I would guess.

1 Like