Yeah. It HAS to be a magical syntactic construct. Having else pass invites more complex expressions (should 1 if cond else pass if othercond else 2 be allowed?), and there be dragons.
IIUC you pose
return a + (… if False else pass)meanspassreturn func(… if False else pass)meansreturn func()
But that’s inconsistent with existing situation, in which
return a + (…)meansreturn a.__add__(…)
I don’t understand the appeal of the seemingly redundant else pass? Nor why we have to invent a meaning for every possible placement of an inline if or pass instead of just letting everywhere other than the couple of usages that sound useful continue to be a syntax error?
I’m not sure if this is a shared preference, but I’d rather that the pass expression idea be discussed separately from the idea of a “dangling if” being interpreted as omission.
IMO pass as an expression is not a good idea, but dangling-if expressions are interesting.
Limiting the idea to function call and collection literals is important regardless. Start small and prove that it’s a good idea in limited contexts. It can always be expanded later.
If return pass is equivalent to just pass, then the whole return ... if ... else pass would be equivalent to
if ...:
return ...
so it does not make sense to introduce another more verbous (using extra else pass) way of doing the same thing. Even without else pass, the return ... if ... would be simply equivalent to the above two-liner, which is IMHO much easier to read. So I don’t think this idea makes sense for the returns (either with or without pass).
However, I like the general idea of “dangling if” in function calls (without using else pass/omit), and even more though in list/dict definitions as listed above, where it naturally extends the comprehension syntax as in [x for x in a if ...] as also noted above.
I came to the same conclusion myself.
Whether return pass is equivalent to return or pass, the form return <value> if <condition> else pass can already be written on one line as either return <value> if <condition> else None or if <condition>: return <value>.
We wouldn’t gain anything either way.
That is true, and I support that side, but does it make sense to explain that decision with ‘we can already do that in one line’. Because if that was the sole reason a expr if cond pattern for return, then one could also just say, that nearly everything here can be done in one line already, just because ; and exec/eval exist.
IMO that’s just not too much of an argument, even though I think the same. The original reason I thought about return was because I was asked, what that would mean.
I’d be fine with just going back to everything I’ve said here.
Another way around is to have a Vanish sentinel, it is a pretty simple concept :
Any variable being assigned the value Vanish is immediately deleted.
Yet, I wonder if the sole existence of this concept does constitute a global paradigm break.
It does. What e.g. should some fn(Vanish) do? Or some Vanish.attr. Or basically anything else.
Would fn here be called with no arguments? Would it even be called?
Imo the expr if cond would by far be the cleanest, simplest and most Pythonic solution.
I was the one who introduced the else pass idea in this thread, I will try to clarify what I had in mind. My objective was to fix the problem that fn(arg if cond) looks like an incomplete ternary operator. To avoid this, I propose to use the syntax construct fn(arg if cond else pass). I don’t propose to transform pass to an expression that could be used everywhere, it would be used only explicitely and in very specific cases:
foo(1, 2 if True else pass) # positional argument
foo(1, b=2 if True else pass) # keyword argument
[1, 2 if True else pass] # list element
{'a': 1, 'b': 2 if True else pass} # dict value
{1, 2 if True else pass} # set element
Arguably, it replaces an “incomplete ternary operator” with “something that looks like an expression but isn’t”, so in the end it may be worse.
To be clear, I don’t propose to allow a = pass or return pass, as discussed before it becomes a can of worms. That means that foo(1, 2 if True else pass) would not be equivalent to a = pass; foo(1, 2 if True else a).
That said, I find this argument very compelling. [i*i if i%2 ==0] looks like a list comprehension without the for i in range(10) part.
It does! Whether it looks too much like a list comprehension is another question, though.
Since there’s a silence for 2 days and no core dev seems interested: May i consider this as dead / non-PEP-able?
Oh, seems like I didn’t remember correctly what I thought vs what I wrote. The way you describe it, x if y else pass would be a perfect fit, even though it might get confused for ternary expressions.
Not necessarily: it could be that no one has any further objections, or are awaiting responses to their questions. If the former, then creating a PEP is the next step.
In order to write a PEP, you need to find a sponsor, and to find a sponsor, you basically need a “live and interesting idea” that catches someone’s attention. So I think something more is needed here if anyone thinks this can overcome that barrier.
IMO, the best way forward, if anyone is going to seriously pursue this, is to pull up real examples of existing code and show how the syntax improves those examples. (And then we can debate about whether or not it’s an improvement, of course!)
To me, and to my taste, the most compelling looking cases for a language change are:
- building a
kwargsdict which you then**kwargs-expand - conditionally omitting elements from collection literals
I decided to go fishing for cases of the former in the stdlib, to see what I’d find.
here's a quick little script to find kwargs = {}
import ast
import glob
import pathlib
class Visitor(ast.NodeVisitor):
def __init__(self):
self.nodes = []
def visit_Assign(self, node):
if (
len(targets := node.targets) == 1
and isinstance(target := targets[0], ast.Name)
and target.id == "kwargs"
):
if (
isinstance(value := node.value, ast.Dict)
and len(value.keys) == 0
):
self.nodes.append(node)
self.generic_visit(node)
def kwargs_defs(parsed_module):
visitor = Visitor()
visitor.visit(parsed_module)
yield from visitor.nodes
for fname in glob.glob("Lib/**/*.py", recursive=True):
if fname.startswith("Lib/test/"):
continue
parsed_module = ast.parse(pathlib.Path(fname).read_text(), filename=fname)
for node in kwargs_defs(parsed_module):
print(f"{fname} {node.lineno}")
The results are somewhat lackluster. Maybe the stdlib is unusual in not using this pattern much, since it doesn’t tend to wrap APIs from another source. Here are the hits on current main (5ec03cf3b0):
Lib/inspect.py 2888
Lib/threading.py 906
Lib/sched.py 70
Lib/subprocess.py 1770
Lib/unittest/mock.py 2575
Lib/logging/config.py 715
Several of these don’t, on inspection, fit the pattern. But the one in logging.config is interesting because it is about fitting the API of some external class – in this case a user-defined formatter.
Here’s the extracted snippet, sans context:
kwargs = {}
# Add defaults only if it exists.
# Prevents TypeError in custom formatter callables that do not
# accept it.
if defaults is not None:
kwargs['defaults'] = defaults
# A TypeError would be raised if "validate" key is passed in with a formatter callable
# that does not accept "validate" as a parameter
if 'validate' in config: # if user hasn't mentioned it, the default will be fine
result = c(fmt, dfmt, style, config['validate'], **kwargs)
else:
result = c(fmt, dfmt, style, **kwargs)
Here’s a plausible rewrite of the relevant section if we have if-expressions for conditional elements of collection literals and function call keyword args:
# Add "defaults" only if it exists and "validate" only if it is defined in
# the config. These conditions prevent TypeErrors from custom formatter
# callables that do not accept these arguments.
args = (
fmt,
dfmt,
style,
config["validate"] if "validate" in config,
)
result = c(*args, defaults=defaults if defaults is not None)
(note that I didn’t assume that we can use the dangling-if on a non-keyword arg)
I would argue that the intent of this code is clearer than the original. But you could say that this is an apples-to-persimmons comparison, since the new version also eliminates branching variants of a function call in a way which is possible today. Here’s what it would look like rewritten to be similar in style, but using only existing language features:
# Add "defaults" only if it exists and "validate" only if it is defined in
# the config. These conditions prevent TypeErrors from custom formatter
# callables that do not accept these arguments.
args = (fmt, dfmt, style)
kwargs = {}
if "validate" in config:
args += (config["validate"],)
if defaults is not None:
kwargs["defaults"] = defaults
result = c(*args, **kwargs)
And then, finally, here’s what it would look like if we consider the proposal to include dangling-if not only for keyword args, but also for positionals:
# Add "defaults" only if it exists and "validate" only if it is defined in
# the config. These conditions prevent TypeErrors from custom formatter
# callables that do not accept these arguments.
result = c(
fmt,
dfmt,
style,
config["validate"] if "validate" in config,
defaults=defaults if defaults is not None,
)
Perspectives will vary – I’m sure some prefer the original version above any of these – but I like that final version pretty well. The comment and context also makes a pretty good case for not just passing along defaults: in this case, we’re trying to match the signature of a callable we’ve been given as an input, so we don’t even know if an argument is supported, let alone supported with some specific default.
After getting so few potential results in the stdlib, I was tending against the proposal, but I think this example is actually pretty compelling. I’m still at a sort of weak +0 on the whole idea because I think the syntax risks confusing a lot of people: x if y and x if y else z look very similar but would have very different rules for proper usage.
IMO, a couple more examples like this from major projects might be enough to convince a core dev that it’s worth helping with a proposal. So, to bring it all home…
If you’re done with the exploration and don’t want to try to find evidence to convince or attract people to the idea, yeah, it’s probably dead (unless someone else picks it up). But you don’t have to let it go at this stage.
IMO the idea has merit, but finding an acceptable “spelling” for omission of parameters and collection members is tricky. I’m not sure that the “dangling-if” construct is the right path.
Only if there’s a core dev willing to sponsor it.
I’d have expanded the script to also include kwds, ns, dict, dictionary, mapping and other similar names. **kwargs is the standard for function parameters that take any keyword arguments, but looking at the stdlib other names are also commonly used. I’ll run the modified script today (for the 3.15 stdlib), and share the results here.
After a quick search, I found two examples in my codebase, both related to constructing a list of arguments for a subprocess command.
The first one is very simple:
args = []
if new:
args.append("--new")
args.append(os.fspath(filename_sarp))
subprocess.check_call([self.exe, *args])
would become:
# one line
subprocess.check_call([self.exe, "--new" if new, os.fspath(filename_sarp)])
# One line per argument
subprocess.check_call([
self.exe,
"--new" if new,
os.fspath(filename_sarp),
])
# one line, using pass
subprocess.check_call([self.exe, "--new" if new else pass, os.fspath(filename_sarp)])
# One line per argument, using pass
subprocess.check_call([
self.exe,
"--new" if new else pass,
os.fspath(filename_sarp)
])
The second example is a bit more complicated, but still quite simple:
args = [os.fspath(f) for f in filenames]
if generic_name:
args.append(f"--generic-name={generic_name}_slc")
if output_dir:
args.append(f"--output-dir={output_dir}")
subprocess.check_call([self.exe, "sar", "process", *args])
would become:
# One line per argument
subprocess.check_call([
self.exe,
"sar",
"process",
*(os.fspath(f) for f in filenames),
f"--generic-name={generic_name}_slc" if generic_name,
f"--output-dir={output_dir}" if output_dir,
])
# One line per argument, using pass
subprocess.check_call([
self.exe,
"sar",
"process",
*(os.fspath(f) for f in filenames),
f"--generic-name={generic_name}_slc" if generic_name else pass,
f"--output-dir={output_dir}" if output_dir else pass,
])
In conclusion, I think that the new version expresses the intent better and has the advantage that everything happens at one place with no additional blocks and indentation levers, but in both cases the original version wasn’t that complicated. The new syntax would be nice to have, but it’s not groundbreaking in those examples.
That fits with my intuition here. The new syntax (without pass - I really dislike the else pass version) is an improvement, but only a small one. And I suspect there are plenty of cases where the new syntax would get over-used, in situations where it’s not an improvement[1].
It’s not the fault of the proposal that people could misuse it, but it does influence the decision of whether the feature would be a net benefit to the language ↩︎