PEP 736: Shorthand syntax for keyword arguments at invocation

Can you give a code snippet spelling out what you mean by transparently and non transparently passed kwargs?

1 Like

I wouldn’t use it in a case like this:

def foo(*, x, y, z):
    bar(x=, y=y*2, z=)

This incongruity invites extra thought by anyone reading it as to why only one argument that is named the same is being passed through to the inner function and if that’s a mistake, I’d spell it out:

def foo(*, x, y, z):
    bar(x=x, y=y*2, z=z)

My opinion of the feature (not just from how it’s been proposed here, but from use in other languages that have it, such as js) is that it is only an increase in readability in similar cases that kwarg unpacking would be used as any case less simple than that leads to situations where something being different draws extra mental attention to something just by virtue of being a noticeable difference, even when that isn’t desirable.

5 Likes

I would argue that the extra thought is good there: there is one argument that is unlike the others, so it’s good to draw more attention to it.

6 Likes

In a trivial case like the above, maybe. Real-world experience with this in other languages has led me to be rather negative on this as a “Feature” as many cases aren’t that trivial in terms of something being different and find themselves arising in a very different manner. It becomes more obvious in more complex cases, but this is a feature I largely see myself not only not using, but writing CI checks to forbid in code I maintain.

1 Like

I can’t really give good real-world code examples of this, as it’s a pattern I don’t use in personal projects, and in work-related stuff, it’s closed source.

The problem arises when a function is doing relatively mundane things in a domain where variables have a consistent meaning. When considering composing various transformations, the transformations are mundane. Every single time things aren’t passed through as-is is just as mundane and shouldn’t require extra thought. These functions are frequently pure math, heavily tested, and only ever get changed when a formatting tool changes its rules :roll_eyes: but the incongruent structure this pep would have would invite extra thought on every single one.

1 Like

While Mike didn’t assign that transformation to a variable prior to passing it in, let’s pretend he did and it was not y. That custom y isn’t actually different in any kind of relevant way versus x and z except incidentally that the name chosen for the variable being passed in doesn’t match the name of the function parameter. So why should that argument visually stand out?

Your single call’s use case may conceptually be a tiny subset of the overall generalization the function is handling. What the function calls asof_date, I call process_date because that name has meaning relevant to my code. But what the function calls timeout may also be timeout to me as well, and a lack of symmetry between those would be visually jarring and call your attention to it without a good reason for doing so.

So to that extent I agree with Mike that I probably wouldn’t mix the two forms.

3 Likes

Are the variable names actually things like x, y, etc. E.g. single letter variables names? These make sense in math contexts.

I could sympathize with an argument that this feature is most useful when keyword parameters have slightly longer names, otherwise they’re not really saving any keystrokes and the decreased readability/increased implicitness (I know this is a controversial word in this topic) are not worth it.

In fewer words, this can be a very good feature for medium to long keyword parameter/argument pairs, but for very short keyword parameter/argument pairs it may just be annoying.

@mikeshardmind I can’t actually tell if having short variable names is important to your proposed example or not. If not then it seems to me like you don’t like when only a subset of the passed-through arguments get the PEP 736 treatment because it somehow looks like a mistake?

IMO (and OT), such functions are better written using positional-only params[1].


  1. just my two :coin: :coin: ↩

1 Like

I think this is very much on-topic. Many of the examples involving maths-type functions are distractions, imho. These functions have well-understood positional parameters, and as soon as someone started to use keywords, I would questions what’s going on. For example, hyp2f1(z=x, a=u, b=v, c=w) is legal, but would never pass review, because it takes time to mentally reorder the arguments.

2 Likes

I shared this in the other thread, but it’s too buried at this point. It’s very much the sort of case being described above, in which the “switching cost” of reading different calling syntaxes far outweighs any benefit in information density.

Here’s a bit of Ruby from a project where Rubocop “forced” me to adopt punning style. Only one parameter, maxdepth, is punned, and I believe it makes the code marginally less readable due to the unnecessary incongruence between variable passing styles:

def render_menu(
  item_descriptors, depth: 0, maxdepth: 1, collapsible: false
)
  # render each item in the menu
  rendered_items = item_descriptors.map do |item_desc|
    # do we have subsections to render or not? (only if there is depth left)
    has_subsections = (
      (maxdepth - depth).positive? &&
      item_desc[:subsections] && item_desc[:subsections].length.positive?
    )

    # create the link to the current item
    rendered_item = link_to(
      item_desc[:item][:short_title], relative_path_to(item_desc[:item])
    )           
                
    # combine with the subsections if there are any to render
    if has_subsections
      rendered_item = html_tag(
        'div',
        %W[   
          #{rendered_item} 
          <button class="caret"
          aria-label="expand/collapse #{item_desc[:item][:short_title]} submenu">
          <span class="caret"></span>
          </button>
        ].join(' '),
        { 
          class: 'sidebar-heading',
          role: 'navigation',
          'aria-haspopup': 'true',
          'aria-expanded': 'false'
        }
      )
      rendered_item += render_menu(
        item_desc[:subsections],
        depth: depth + 1,
        maxdepth:,
        collapsible: true
      )

      html_tag('li', rendered_item, class: 'sidebar-submenu')
    else
      html_tag('li', rendered_item, class: 'sidebar-leaf-item')
    end
  end.join
    
  tree_class = "sidebar-tree sidebar-tree-l#{depth}" + (
    collapsible ? ' sidebar-tree-collapsible' : ''
  )
  html_tag('ul', rendered_items, class: tree_class)
end

I note that this demonstrates that the syntax applies naturally to recursive functions which pass forward parameters.


I would like the AST nodes to be different, if only by a single bool flag, so that it’s possible to write AST-based linters which could call the following Python usage out as “don’t mix styles”:

render_menu(
    item.subsections,
    depth=depth + 1,
    maxdepth=,
    collapsible=true
)

I don’t know that I would write such a linter. But I’d like it to be possible.

11 Likes

No, The variable names are not always short, and again, I can’t share my real-world examples for reasons already stated. I think calling it pure math left people thinking these were simpler cases than they actually are.

The length of the variable names doesn’t matter, the point is that when you have a situation like

f(
  some_unimportant_variable_name_passed_through=,
  some_other_unimportant_variable_name_passed_through=,
  some_transformed_paramater=transformed_value,
)

you’re inviting extra cognitive overhead on every single case of such when there’s really no need for it, the question of “why is this parameter different” is not something that needs to be thought of every time, but the fact that this has an incongruency will be visual noise every time, because people are good at spotting things that are like this, and it draws mental attention to it.

The fact that we have two maintainers of a super opinionated autoformatter here saying they’d enforce it everywhere tells me everything I need to know about how this will play out in the real world. People will push to use this everywhere just like they have in other languages that have it (js, ruby) even when it’s inappropriate and just extra churn and mental overhead.

I don’t think this brings anything better than the current patterns using kwargs and allowing passthrough args to be consistently passed through with unpacking (unambiguously not a mistake), and I think it brings significant negatives that I’ve encountered in real code in other languages.

10 Likes

Ok yeah, that plus the last round of discussion is clarifying.


So the further objection is that people don’t like the PEP 736 eliding used side-by-side with cases where the PEP 736 eliding is impossible due to parameter/argument name mismatch.

In other words, there’s a subset of people who might be ok with PEP 736 eliding if it is in a function call where every keyword argument uses PEP 736, or no keyword argument uses PEP 736, but they’re most unhappy when there is a combination of the two.

4 Likes

I think this is true for me, although I imagine I might get used to it. I think it would partially become a matter of additional style conventions: maybe grouping the elided keywords together would make them stand out and easier to understand in a quick skim.

1 Like

Yes, that’s my thinking exactly. I’m not too bothered by it but I think I would (1) do as you say and group elided keywords together and (2) my brain would start to fill in foo(x=) as foo(x=x) the same way it fills in f"{x=}" as f"x={x}".

Precisely. I might get comfortable with cases where the new form was used for every parameter, but I cannot imagine ever being comfortable with a mixture. As @mikeshardmind says, the cognitive load added by having to track which form is being used would be far more of a cost than any small gain that might be achieved.

Strong +1 from me on this. It never occurred to me that anyone would use the same AST for the two different forms, and being able to distinguish so that a linter could report on mixed styles seems like an important use case to me.

14 Likes

I found another case of prior art: the Jakt programming language GitHub - SerenityOS/jakt: The Jakt Programming Language

1 Like

Passed as a bare name? Ambiguous with positional arguments, but this is an interesting case because all args are named by default.

The most useful case for me is the mixed case, and the examples here with mixed case, appear (at least to me) readable. After paying attention to how I’d use the syntax for a couple weeks, my most common use was wrappers that had more context on some parameters, but not others. e.g.

def anonymous_foo(url, bar, baz, qux):
    stripped_url = strip_tracking(url)
    _foo(url=stripped_url, bar=, baz=, qux=)

I agree with

The local variable above could be written url = strip_tracking(url), but a formatter then eliding url=url would be a mistake IMO. Formatters should only clean up syntax that can’t be used to communicate intent.

But I’ve moved from iffy on this proposal to definitely a supporter, whether * or =.

1 Like

I think it also comes up a lot in web apps to “pass” variables to templates. Example (not real, but not unrealistic I think):

from fastapi import ...

app = FastAPI()
templates = ...
config = ...

@app.get("/widgets/{category}")
async def widgets(request: Request, category: str):
    widgets = ...
    widget_count = len(widgets)
    pagination = Pagination(widget_count)

    # With PEP 736:
    return templates.TemplateResponse("widgets.html",
        dict(request=, category=, widgets=, widget_count=, pagination=, config=))

    # Without PEP 736 currently typical:
    return templates.TemplateResponse("widgets.html",
        {"request": request, "category": category, "widgets": widgets, "widget_count": widget_count, "pagination": pagination, "config": config})

# Many similar web app endpoint functions here ...
2 Likes

In UI applications and machine learning code passing many arguments with the same name is also very common.

Here are random real examples copied 1:1 from github:

        kwargs = {
            'rgb': rgb,
            'colormap': colormap,
            'contrast_limits': contrast_limits,
            'gamma': gamma,
            'interpolation2d': interpolation2d,
            'interpolation3d': interpolation3d,
            'rendering': rendering,
            'depiction': depiction,
            'iso_threshold': iso_threshold,
            'attenuation': attenuation,
            'name': name,
            'metadata': metadata,
            'scale': scale,
            'translate': translate,
            'rotate': rotate,
            'shear': shear,
            'affine': affine,
            'opacity': opacity,
            'blending': blending,
            'visible': visible,
            'multiscale': multiscale,
            'cache': cache,
            'plane': plane,
            'experimental_clipping_planes': experimental_clipping_planes,
            'custom_interpolation_kernel_2d': custom_interpolation_kernel_2d,
            'projection_mode': projection_mode,
        }
super().__init__(
            dimension=dimension,
            description=description,
            intensity_range=intensity_range,
            pix_dim=pix_dim,
            lamda=lamda,
            sigma=sigma,
            config=config,
        )
self.dice = DiceLoss(
            include_background=include_background,
            to_onehot_y=to_onehot_y,
            sigmoid=sigmoid,
            softmax=softmax,
            squared_pred=squared_pred,
            jaccard=jaccard,
            reduction=reduction,
            smooth_nr=smooth_nr,
            smooth_dr=smooth_dr,
            batch=batch,
        )

(It’s not difficult to find many more or longer examples.)

I think these would benefit a lot from PEP 736.

4 Likes