I totally agree. This proposal can actually be viable if we scale back the scope to not allow pass as an actual value (so the nice use case of return pass from a mapper function would be sacrificed for the main goal of expressing explicit absence in a container literal or argument list).
Regarding the new possibilities to write letās say ālow-quality codeā like numbers = [pass, 1, pass, 2, pass] etc. Iād like to note that we could write this all the time:
if condition:
pass
print("OK")
pass
else:
pass; print("ERROR")
and still nobody is afraid that it will happen in real life. I see no reason to expect a change in this regard in connection to what is being discussed here.
Nobody has asked, but I would join the group preferring that pass remains a keyword (i.e. not a value) and will get the āIām not hereā meaning in collections and comprehensions.
Maybe the cleanest way to implement something like this would be to not make pass itself be an expression but to make the full expr if expr else pass a pattern that exists on the grammar level just below the display syntaxes. So for lists, tuples and sets its a top-level expression but for dicts its a whole key-value pair. E.g. something like this:
[1, 2 if something() else pass, 3]
{
1: 2,
3: 4 if something() else pass,
5: 6,
}
rather than allowing these:
[1, (2 if something() else pass) + 3, 4]
{
1 if something() else pass: 2,
}
This would guarantee that not only is this used in collection literals, it also only is used when it actually makes sense rather than just as an no-op. Though as Iām writing this Iāve now realised that the initial point of using this for an empty set requires exactly the no-op usage, so this would then become an entirely different idea.
This could theoretically also be extended to a return pass form that could e.g. internally create some kind of sentinel object, but only allows that object to immedietly be consumed by a collection creation loop or something like that. If that value is returned in any other context, an error is raised immedietly. That way youād still have the filter map functionality without really introducing a new sentinel with magic behaviour that can be passed around as wanted.
I agree with the gist of your suggestion, which I think is in line with what others have been suggesting in this thread. I was planning on working on an implementation this weekend ![]()
As for return pass, I agree with the general consensus in this thread that pass shouldnāt be an object that users can get their hands on; and detecting the use of a function call that uses the return pass idiom is hard if pass isnāt an object. So I would personally be against extending things in that way.
Iāve been thinking about this a little bit more today, and I pretty much agree with what I think is the general consensus in the thread:
- Some form of this idea is useful, and
- narrowing the scope (conditional expressions only, and only in certain contexts) would get rid of a lot of the weird cases and make it a lot more palatable.
There is still a question of where exactly this thing should be allowed/disallowed. Looks like thereās broad agreement about:
- Single items in literal list/set/tuple displays:
[..., x, y if condition else pass, z, ...] - Full key/value pairs in literal dict displays:
{..., k: v if condition else pass, ...} - Keyword arguments:
foo(..., x, a=b if condition else pass, y=z, ...)
And some other things that might be more divisive[1]:
- Positional arguments:
foo(..., x if condition else pass, ...) - Unpackings of various kinds:
[..., x, *y if condition else pass, z, ...]foo(x, y, **d if condition else pass)
- Others?
Separately, the more I look at this, the more I think that syntax like x if condition() else pass does suggest that pass is a normal expression of some kind, rather than a keyword.
Iām still oscillating, but I think right now I prefer @15r10nkās suggestion from the thread I linked in a footnote above, which is to allow omitting the else clause from conditional expressions in some situations, rather than allowing pass to be used inside of conditional expressions in some situations, so instead of:
my_tuple = (
"first_item",
"second_item" if condition else pass,
"third_item",
)
we would have:
my_tuple = (
"first_item",
"second_item" if condition,
"third_item",
)
And instead of:
foo(
a=1,
b=2,
c=3 if condition else pass,
)
we would have:
foo(
a=1,
b=2,
c=3 if condition,
)
This loses the pass idiom as an explicit indicator that weāre excluding something, but to my eyes itās just as clear, and with quite a bit less typing
. Of course, we could also make the else pass optional to mirror the way you can always add an else: pass to a conditional statement right now.
I quite like these though
ā©ļø
Be careful of citing āconsensusā as though it carried weight. What you have is the consensus of those who have chosen to speak up. There may be many other people who simply donāt consider the proposal worth discussing, and have remained silent.
Naturally. Thatās all I was trying to say.
How about one of the original goals, for specifiying an empty set? e.g. {pass}
It may also be used a quick way to temporarily disable a keyword argument without actually removing it:
f(x, option1=True, option2=pass)
So we may allow pass in a container context whether or not it is part of a conditional expression.
The caller knows what itās calling and the arguments itās explictly calling with so using pass as a positional argument should be allowed, especially useful for functions taking variadic arguments. What shouldnāt be allowed is for pass to be passed around as a value to unsuspecting code.
Allowing pass in an unpacking expression may work but is currently already supported with an empty tuple or dict, so probably not worth the effort here even it offers a small benefit of avoiding an object creation.
- Burying
passin a nested conditional expression cannot be allowed because that requirespassto be an actual value to be passed around.
For example, the following use case may seem to make sense:
[x if condition else y if condition else pass]
but cannot actually work because itās evaluated as:
[x if condition else (y if condition else pass)]
in which case pass is actually not in the context of a container literal.
passshould also be allowed in the truthy output of a conditional expression, e.g.pass if condition else x, to avoid unnecessarily having to write it with anotasx if not condition else pass.
Allowing else pass to be omitted can technically work but might arguably make the meaning of the existing if clause in comprehensions somewhat ambiguous:
[i for i in lst if condition]
even though pass isnāt allowed there in the first place:
# not allowed because pass is not actually in a container context
[i for i in lst if condition else pass]
I otherwise like the idea too.
Thanks for the demo Adam! Iāve enjoyed playing with this syntax.
Because the condition is tied to a single variable, Iām wondering when this might be useful. How often is only one variable changing as a result of a condition? Perhaps one useful comparison is with existing if-statement based solutions. For instance,
obj = {"a": 1, "b": 2 if cond else pass}
vs what we can do now,
obj = {"a": 1}
if cond:
obj["b"] = 2
In my opinion, the existing solution better extends for multiple effects given a single condition, while the proposed syntax may encourage repeating the condition even for related items.
For tuple construction, some repetition would be required with an if-else block, or using a *(x if cond ()) style construction which is admittedly harder to understand than the proposed syntax. However, Iām not sure Iāve needed a tuple that can sometimes be one length and sometimes be another.
For function calling, one could always construct a kwargs in a similar way to the dictionary example, and then unpack. But there is a question of how should functions be designed. I treat the proposed syntax in this setting as a way of saying āI donāt care what this parameter is when the condition is Falseā, which feels similar to how None is commonly (but not always) used. Perhaps this syntax could help eliminate duplicating function defaults in callee code.
I think positional arguments should be allowed.
Itās perhaps rare for it to be a good idea, so linters would probably quickly make rules prohibiting it. But thatās what linters are for.
There are problems that can be solved elegantly using conditional positional arguments, and it would be really confusing + frustrating if those were prohibited out of a concern to āprotect the programmerā.
For unpackings Iām happy to use *() and* **{} already, so I donāt see an urgent need. But no great reason to prohibit it either.
It would be a bit sad if [x if condition else y if condition else pass] (or [x if condition else y if condition]) were unavailable. A programmer could fix it using brackets: [(x if condition else y) if condition else pass]. I would have expected that the interpreter could do the same.
On second thought pass in a nested conditional expression can actually work as long as the output expressions in the grammar rule for the container version of the conditional expression propagate the container context. Itās only when the expression contains other operators that the container context stops propagating.
On that note we may also allow [x or pass] to work by allowing or to propagate a container context. Itās a shorthand for [x if x else pass] except it evaluates x only once. [x and pass] can theoretically be allowed too for consistency sake but may be marginally useful only to silently evaluate x without outputting it.
Thank you for mentioning me, but I have to say that I like your idea more because it has several advantages over mine.
- I had several comments for my idea that the missing else is unfamiliar. I think that this idea solves it in a very nice way because it is very similar to the existing
a if cond else bsyntax. - It allows code like
func(pass if error else value) - It also composes very nicely when you needed it
func( value1 if condition1 else pass if condition2 else value3 if condition3 else pass )
The f(value if condition) syntax could also be allowed (some years) later when everyone if familiar with the way how else pass works in if-else expressions and wants to omit it, but I see this part as optional.
I also have some open questions:
What is in cases like:
d1={"a":1, "b":2}
d2={**d1, "a":pass}
is d2=={"b":2} or is d2=={"a":1, "b":2}?
What would f(**d1, a=pass) do?
A key with pass as a āvalueā should be treated as if the key isnāt written, so d2 == {"a": 1, "b": 2} and f(**d1, a=pass) == f(a=1, b=2) IMHO.
yes, this is the simplest rule and should be easy to understand
. You can always create explicit code when you want to omit b like in my examples.
You mean to omit a in your example right? That wouldāve been a great use case too, since people have been asking for a more expressive way to create a copy of a dict without a selective few keys, so if {**d1, "a":pass} actually meant to delete key "a" after unpacking d1, it wouldāve satisfied the use case, though ultimately I find making pass always omit the literal entry keeps its meaning more consistent across its usage. There can probably be a separate proposal for something like {**d1, "a":del} in the future.
Yes, thatās exactly what I meant. Treating is as if the key is not written will also simplify the implementation because you donāt have to check for an existing a in the dict everytime you use an expression with pass. I also like the idea with the explicit del.
Besides yield and pass, there are some keywords (raise, del) which are statements but can possibly take on expression roles. But not all of them will be useful or have meanings consistent with statement usage when used as expressions, or there are better alternatives that donāt require turning the keywords into expressions.
del
Excluding unwanted keys from an existing mapping is possible to do in one line:
{k: v for k, v in d1.items() if k not in {"a"}}
raise
Writing a custom function for raising an exception looks like itād work
def raise_func(ex: BaseException | type[BaseException]):
if isinstance(ex, BaseException):
raise ex
raise ex()
s = [
...,
some_function() or raise_func(ValueError("None")),
....
]
but not actually as intended. The raise_func function itself counts as a stack frame in the traceback. The traceback is written when the exception is actually raised. Writing a function to emulate the traceback creation process in pure Python is not trivial, not to mention issues with exception chaining and contexts.
To raise exceptions inline, raise does not need to become an expression(if ignoring cases of raise from). It can instead be made into a builtin function or a method of the BaseException object:
s = [
...,
some_function() or raise_now(ValueError("empty string or None")), # or
some_function() or ValueError("empty string or None").raise_now(),
....
]
pass is currently a no-op statement and turning it into an expression will not cause behavior incompatible with current usage.
Because of short-circuiting, if bool(x), [x or pass] would output [x]; and if not bool(x), [x and pass] would output [x] if I understand correctly, e.g.:
>>> for b in False, True: print(b, [b or pass_], [b and pass_])
False [] [False]
True [True] []
(edit: pass_ is an instance of a class with repr() == "")
Youāre absolutely right! [x and pass] would be useful where x should be retained only when itās falsey. Thanks for the correction.
Perhaps BaseException subclasses could inherit a __call__. This would only feel natural imo, as one could do
Exc = Exception(*args)
Exc() # raise type(Exc)(*args)
# Or:
Exc.__call__()
so conditionally people could do t = (arg if is_valid(arg) else ValueError("bad argument")()) too.