Playing With the Idea of `pass` as an Expression

Continuing the discussion from PEP 802: Display Syntax for the Empty Set:


In the other thread, @matthewyu0311 made a suggestion about pass as an expression that I thought was cool[1]. This change would be backward-compatible in that pass could still be used everywhere it’s currently used, but it would also open doors to cute inline things like subprocess.run(["command", "--debug" if debug else pass]) (a variant of code suggested by @bwoodsend in the other thread).

I have a hard time thinking about things like this in the abstract, though, so I spent some time today hacking at an implementation. The resulting code is a disgusting hacky mess and definitely doesn’t handle all edge cases, but it did let me get a sense for how this might feel in practice.

In case anyone else is interested, I’m sharing an Emscripten-based demo of the implementation (source at adqm/cpython:pass_expression if anyone wants to compile for themselves, but please don’t judge the code too harshly :wink:). Here is what I tried to implement:

  • pass becomes a built-in constant like None.

  • pass cannot be added to a list or tuple or set; trying to do so will cause that entry to be (silently) ignored. So, for example, [1, 2, 3 if False else pass, 4] == [1, 2, 4].

  • Similarly, pass cannot be added as a key or a value in a dict. So, for example, {1: 2, 3: pass, pass: 6} == {1: 2}.

  • If pass is provided as an argument (positional or keyword) to any function call, it’s ignored as well. So print(1, pass, 3, file=pass) is equivalent to print(1, 3).

  • So that I could try out some of the other suggestions from the thread, I added / as an alias for pass. So in the demo, {/} == {pass} = set().


To be clear, though, I’m not actually proposing that this change be made, just sharing some thoughts and code.

I do think there’s something cool here. When used in the right way, [expr if cond else pass] feels to me like a very natural extension of what we already have.

But after trying it out some more, I’m pretty much convinced that this is too weird to be considered seriously, at least without some adjustments to the semantics I implemented above. Maybe it would feel normal after enough time, but for now I don’t think it feels like the natural extension of the language that I was hoping for.

Here are a couple of things that weirded me out a little bit at first even though they’re natural consequences of the semantics described above:

>>> pass
pass
>>> print(pass)  # no output
>>> type(pass)
Traceback (most recent call last):
  File "<python-input-11>", line 1, in <module>
    type(pass)
    ~~~~^^^^^^
TypeError: type() takes 1 or 3 arguments
>>> x = pass
>>> x
Traceback (most recent call last):
  File "<python-input-19>", line 1, in <module>
    x
NameError: name 'x' is not defined

  1. It also reminded me of another old thread ↩︎

21 Likes

Yes, this is not going to work. There are far too many difficulties with making something that is both a value and not a value. SQL’s NULL value is less quirky than the pass you’re proposing, and it’s mind-meltingly bizarre in its inconsistencies.

I don’t think this thread even belongs in Ideas.

4 Likes

That may be fair; totally fine if it makes sense to move it. I wasn’t trying to generate spam.

It’s just that this idea and other similar ideas (like @blhsing’s original proposal for / and Absent) were generating a lot of (IMO off-topic) discussion in the PEP 802 thread, so I think it makes sense to have a dedicated place to talk about those kinds of ideas if there’s still more to be said.

You’re right that it’s off-topic for that thread; I just don’t think that it’s a workable idea at all, as the entire concept doesn’t fit into Python’s model.

1 Like

I quite like this despite the quirks. Playing with that REPL was really satisfying. And being able to conditionally set keyword arguments was a nice bonus I wasn’t expecting but have found myself wanting in the past.

9 Likes

As a non-element in a list, set, or whatever, e.g. [pass], ok, but as a non-key or non-value in a dict, e.g. {pass: 0}, no.

It should be all or nothing, a complete item or no item.

An incomplete item such as a value without a key or a key without a value is right out! :slight_smile:

And binding a name to nothing, the same.

Incidentally, print(pass) would print something. It would print a newline because it’s just a longer way of writing print().

1 Like

My idea is that pass is not another value like None, but a control flow quasi-expression. What’s hard is to figure out an implementation strategy that meaningfully reflects this nature. Not treating pass as a value will require quite different thinking from parsing the syntax to compilation into bytecode.

My idea is to limit pass (or other kind of absence markers) to strict within literals that contain expression lists, not comprehensions. Comprehensions already have an if clause available, making pass unnecessary. The use of pass in function arguments is not a goal, though sometimes this seems useful for calling poorly-designed functions that are sensitive to the number of arguments but don’t otherwise expose a way to specify a default with None or other sentinel value – pass will encourage the proliferation such poor design.

1 Like

I agree that there’s something cool here. I like pass over / way better. The word is very fitting: ‘pass’. Pass over this item and ignore it. It gives more of a purpose to the keyword that isn’t used very often.

The current proposal as it stands adds a lot of redundant syntax. Aside from the empty set, it never really makes sense to write things like print(pass), [pass, pass, pass], and {pass: pass}. It would contribute great ugliness to the language. If implement, it should only be legal in collection literals or function call arguments, and must be a part of a ternary expression.

2 Likes

I am going to semi resurrect a proposal that I made in a previous discussion: Pass through star-expression unpacking through if-else statements. Then pass = *() for (almost) all of your examples.

Ah, sorry for misunderstanding! Didn’t mean to misrepresent your proposal here!


I definitely agree! Those were not intended to be representative of real code that people would actually write, but merely to demonstrate the behavior in the case where an expression involving pass would end up in any of those spots.


I’m not sure that I would want to be that restrictive. That would preclude the following form, for example, which could be useful:

def prepare_input(x):
    y = do_some_fancy_work(x)
    if condition(y):
        return pass
    return y

z = [prepare_input(i) for i in inputs]

That said, I can definitely see pros and cons for all kinds of tweaks to the specific behavior I implemented here, which is part of the difficulty (there’s no one clear obvious expected behavior).

1 Like

While the idea is awesome as a thought experiment, this behavior is most likely going to cause a great deal of existing introspection tools (a debugger, for example) to produce such an error, and I can’t quite imagine a good solution to allow them to work without requiring all of them to be refactored to special-case pass or /. I think this is the one big hurdle we need to overcome for the proposal to be taken seriously.

BTW I thought about using the keyword pass when I suggested Absence/Absent in the other thread too, but thought pass just doesn’t read like a noun or an adjective so doesn’t quite fit the natural language-like quality of the Python language. But it certainly would be great to make it a hard keyword like None.

1 Like

For better or for worse, one of the things that contributed to my feeling that this isn’t as natural of an extension as I would like is that I don’t see a clear, easy path to implementation. It’s entirely possible that there’s a totally elegant solution and I just don’t see it yet, but it feels to me that a complete implementation of this idea would require a lot of special casing regardless of the specifics.

1 Like

I, for one, very much appreciate your work, @adqm, in exploring how a pass expression might work in practice. That other thread was not the first to propose “disappearing” expressions in collection displays and comprehensions. It seems unlikely this will ever get added to Python, but it seems entirely reasonable to explore.

Considering the specific semantics as you’ve proposed them:

This is OK for an initial toy implementation, but a constant that can’t be assigned to any name or returned from any function seems strange. More on this below.

There have been previous requests for optional inclusion of elements in collection displays. To the extent that’s a desirable feature, using pass is akin to what one might do in an if statement (although it’s more verbose than the proposal to use a bare if clause like in comprehensions).

It also enables the empty set literal: {pass}, with consistent analogs for the empty tuple and list, which would be pass, and [pass], respectively. Moreover, one could write a set display that does not change meaning when you comment out all of it’s contents:

my_set = {
    # "commented out value",
    pass
}
assert isinstance(my_set, set)  # True

The semantics for dict displays are a bit more awkward, but I can’t come up with a coherent reason to justify only allowing pass in the key or value position. I suspect there will be pushback here, but allowing pass in sequences and sets but not mappings would be a dealbreaker,

Optional keyword arguments is a huge win. Currently, using an optionally missing keyword argument requires making a kwargs dict and unpacking it, or it requires finding and using the default value. Neither of these is particularly ergonomic.

Optional positional parameters feels much more ripe for wreaking havoc (particularly for type checking) and offers less benefit (positional parameters are rarely optional outside of unpacking). I would vote that using pass for positional arguments should be disallowed.

To consider your other “weird” cases:

  • print(pass) is equivalent to print() and type(pass) is equivalent to type() - these are only weird if you think of pass as an object, which is not the right mental model.
  • x = pass: Like positional parameters, I don’t think pass should be allowed in assignments. The right structure is to use is an if statement.

Adding my own: what should the following do? (On your current demo, it seems to enter an endless loop)

def f(a, b=pass):
    return a

f(1)

And how about returning pass? Your demo seems to allow it, which suddenly means pass is leaky - you can have expressions which evaluate to pass but which do not themselves contain the keyword pass. That’s a huge footgun. And even if we block return pass, I imagine there’s going to be other unexpected ways it can escape…


Ultimately, if we even want this behavior, the challenge is: how does one define a pass expression in such a way that it can be allowed in some contexts, but not others. An expression must evaluate to a value, which necessitates that pass must be a value, which leads to something like your implementation. But if pass is “just” an object, how does one restrict it from use in foot-gun prone situations (like positional arguments, function defaults, and variable assignments)?

My guess is that the benefits (ergonomic conveniences around collection displays and keyword arguments) do not justify the costs (weirdly disappearing expressions).

6 Likes

Totally agree that would be an important hurdle to overcome. But I don’t have any answers yet, either!


The reason I like pass is that it already has an established meaning in Python: “do nothing,” which I think transfers nicely to this new context :slight_smile:

4 Likes

I’m glad you found it helpful! I’ve been having fun poking at the interpreter, and this forum has been great for providing neat ideas that give me an excuse to do so :slight_smile:


Very much agreed.

I definitely intended for pass to be able to be returned from functions.

The inability to assign pass to a variable was something I didn’t originally intend, but rather a side effect of the other decision not to allow pass as a value in a dictionary. That’s certainly something that could be changed, and something that I waffled about when I was putting my little demo together. But of course, while allowing pass as a dictionary value would solve some of the current weirdnesses, it wouldn’t solve them all. And it raises other questions as well; specifically, the question of what d.values(), d.items(), and d.keys() should return for, e.g., d = {2: pass} is why I didn’t write things that way.

Also, I honestly don’t expect there’s a realistic use case, but there is at least a way in which one could perhaps see the current behavior as internally consistent (maybe this is a stretch):

a = some_initial_value

def prepare_input(x):
    y = do_some_fancy_work(x)
    if condition(y):
        return pass
    return y

# ignore elements for which prepare_input(i) returns pass
z = [prepare_input(i) for i in inputs]  

# NOP (keep the existing value) if prepare_input(a) returns False
a = prepare_input(a)

This is nice! It feels analogous to pass in the body of a conditional allowing you to comment out the rest of that body.


I had not considered that (hence my broken implementation). The only behavior that comes to my mind right away as consistent with the semantics I implemented above would be not to allow pass to be a default argument in the first place, so maybe to make that raise a TypeError at definition time.

That said, if pass were allowed to be a value in a dictionary, then this would be totally OK.


What was going through my mind as I was coding was that I wanted pass to be a plain-ol’ object, but one that can’t be added to collections (including argument lists for function calls). All of the other behaviors pretty much came along for the ride (anything that doesn’t depend on being added to a collection of some kind was intended to be fair game). So basically, I wasn’t trying to keep it from escaping at all :slightly_smiling_face:. That’s not to say that there aren’t better options, just that that’s what I was thinking.


I agree here as well. This is a fun thing to think about, and I can imagine situations where a cleaned-up version of it would be useful, but I think it’s quite unlikely that it will be accepted into the language without substantially more thought to smooth out the (very) rough edges. And even then, it’s probably not super likely.

There’s also the fact that I don’t see a way to implement this such that the resulting code wouldn’t be a pain to maintain.

I might play around with it some more because it’s fun and interesting to think about. But it’s almost certainly going to remain a curiosity.

1 Like

I find the motivating examples here to be really strong. It might be useful to make a list of the best ones. Even if this idea doesn’t take off today, it might be suggested again in the future.

Also, a list of confusing quirks would be good.

6 Likes

Having pass be an actual value has the same problems as any other
proposed “magic” value. Under what circumstances exactly is the magic
invoked? Are we sure it won’t have any undesirable consequences we
didn’t anticipate? How do you suppress the magic when you don’t want it?
Etc.

To me this shows that pass should not be an object, and it should not be possible to assign anything to pass. Then print(pass), >>>pass, type(pass), x=pass and f(b=pass) all work in an intuitive way. The first 2 print nothing, and the last 3 are errors.

That does reduce the flexibility a bit.

4 Likes

Someone in the other thread also made the great point that things would get nasty if functions are able to return both None and pass. Because then there’s no longer a single sentinel for “no answer”. This is yet another reason for pass not to be an object.

5 Likes

The main benefit of pass expression is for static type checkers to check the length and shape of literal sequences. They can follow logic flow around conditionals, but can’t keep track of explicit append calls or filter functions which are semantic in nature.

pass is intended to be a flow control expression within literals. It does not have a value, though implementation strategies may give it a sentinel value (such as, at the bytecode level, pushing a PyStack_NULL onto the stack and have null-tolerant versions of BUILD_{TUPLE|LIST|SET} ignore it).

I don’t think it always needs to be a ternary expression. Other conditions such as [a, b, c or pass] are fine.

6 Likes