Can you give a code snippet spelling out what you mean by transparently and non transparently passed kwargs?
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.
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.
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.
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 but the incongruent structure this pep would have would invite extra thought on every single one.
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.
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?
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.
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.
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.
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.
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.
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.
I found another case of prior art: the Jakt programming language GitHub - SerenityOS/jakt: The Jakt Programming Language
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 =
.
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 ...
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.