Replacement for `else` keyword in `try-else` (for ex. `noexcept` or `not except`)

The issue

I will start with from where I got this idea. During discussion about elif in compound statements some of the points were about how keyword else is often unintuitive. Here are some summaries of understanding the keyword:.

I always understood it differently. Maybe, because I have learned to read assembly before attempting to learn Python, I have never seen any issue with else clause after loops. For me, there are only 2 cases:.

  1. After if, while or for clause, else clause is entered, only if condition check was reached and was false.
  2. After try-except clauses else clause is entered, only if try finished with no exception.

Loops

Reasoning for loops is simple. The for loop is a fancy while loop, so let’s focus on while loop with else clause. Look at the example:.

while loop_condition:
    ...
    if break_condition: break;
    ...
else:
    ...

while loop can be broken into goto jumps (in C):.

loop_start: if (loop_condition) {
    /*...*/
    if (break_condition) goto loop_end;
    /*...*/
    goto loop_start;
} else {
    /*...*/
} loop_end:

Translating more (into assembly) would also break the else block into jumps, but (in short) that’s how I understand loops with else clause.

Try statement

Imagine, U don’t know what else in try statement mean, but U know everything else about try statement and about else keyword in different statements. How would U understand else inside try statement, just by looking at it? Well, in all other cases there exist a clear (on code read) condition to enter the block before else clause:.

if condition: ...
else: ...

while condition: ...
else: ...

for element in collection: ...
else: ...

So, let’s look at try statement:.

try: # No condition.
    ...
except type1: # If any exception was raised,
    # checks if raised exception is of type ``type1``.
    ...
except type2: # If previous condition was checked and was false,
    # checks if raised exception is of type ``type2``.
    ...
else: # ...?
    ...
finally: # No condition.
    ...

Logically, else should do what it always does. Perform actions in its body (“suite”), if previous condition was checked and was false. In other words, exception was raised and didn’t match any of the types in all except checks, or just in the one before else. The complete opposite of what it actually does.

Ok, I kind of cheated. I didn’t include the default except, but note, it still doesn’t make any sense:.

try: # No condition.
    ...
except type1: # If any exception was raised,
    # checks if raised exception is of type ``type1``.
    ...
except type2: # If previous condition was checked and was false,
    # checks if raised exception is of type ``type2``.
    ...
except: # If previous condition was checked and was false.
    ...
else: # ...? There is no previous condition.
    ...
finally: # No condition.
    ...

The only occasion, where else in try statement makes any sense, is when there is no typed except check:.

try: # No condition.
    ...
except: # If any exception was raised.
    ...
else: # "If previous condition was checked and was false"?
    ...
finally: # No condition.
    ...

(Pedantic) note, default except doesn’t perform any checks, it is just jumped into.

Examples of possible replacements

noexcept

try: ...
except type1: # If any exception was raised,
    # checks if raised exception is of type ``type1``.
    ...
noexcept: # If no exception was raised.
    ...

Clear, but possible name collision.

not except

try: ...
except type1: # If any exception was raised,
    # checks if raised exception is of type ``type1``.
    ...
not except: # Negation of ``except``.

Less clear, but still better than else.

EBNF in docs

try_stmt  ::=  try1_stmt | try2_stmt | try3_stmt
try1_stmt ::=  "try" ":" suite
               ("except" [expression ["as" identifier]] ":" suite)+
               ["else" ":" suite]
               ["finally" ":" suite]
try2_stmt ::=  "try" ":" suite
               ("except" "*" expression ["as" identifier] ":" suite)+
               ["else" ":" suite]
               ["finally" ":" suite]
try3_stmt ::=  "try" ":" suite
               "finally" ":" suite

Current EBNF contains an error, the following code isn’t a valid try statement:.

try: ...
except: ...
except Exception: ...

I fixed it, named all clauses (to avoid duplication), and unnamed try variants (naming by numbers, rely?).

try_stmt ::=  "try" ":" suite
              ( try_exc+ [try_dflt] [try_else] [try_fin]
              | try_dflt [try_else] [try_fin]
              | try_grup+ [try_else] [try_fin]
              | try_fin )
try_dflt ::= "except" ":" suite
try_exc  ::= "except" expression ["as" identifier] ":" suite
try_grup ::= "except" "*" expression ["as" identifier] ":" suite
try_else ::= "else" ":" suite
try_fin  ::= "finally" ":" suite

Additional thoughts

I don’t know when typed except was added, but according to docs archive, it existed at least in Python 1.4 (the oldest archive).

Default except with else clause is used quite often, GitHub search shows more than 101k files (I searched by regex with false negatives).

I don’t understand the above. The else can always be trivially seen as entered when the negation of a condition is satisfied and this is the case regardless of any design or language. Just take whichever behavior has been implemented, summarize the conditions under which the content of the else is executed, negate that proposition and that is the (or a) condition that the else is entered when not satisfied.

It can just be “the code inside the try produced an exception”.

Opinions on whether the condition is natural, intuitive, desirable, or not, are fine to have, but not that it doesn’t exit.

Note, default except is flagged by type checkers, thus is probably not considered a good practice.

E722 Do not use bare `except`
Found 1 error.

But one can replace except: with except Exception: and all fine I guess?

I think what @Alex-Wasowicz is doing is analysing:

try:
    do this
except Exception1:
    action 1
except Exception2:
    action 2
else:
    default action

as something like this:

try:
    do this
if it raised Exception1:
    action 1
elif it raised Exception2:
    action 2
else:
    # None of the above, i.e. it could be another exception or no exception.
    default action

Notaby, this isn’t actually the meaning of try/else:

They do different things. Some exceptions are not Exception subclass. eg SystemExit

1 Like

Instead of introducing new keywords, I think except None might be more achievable.

try:
    ...
except:
    ...
except None:
    # only if no exception catched
finally:
    ...
4 Likes

What I meant is that the previous clause didn’t had a checked at entry condition. Thinking about reach condition as the condition the “else” word is referring to, is weird for me, but let’s see:.

# [...]
except: # If check "if raised exception is of type ``type2``"
    # was checked and was false.
    ...
else: # If check "if raised exception is of type ``type2``"
    # wasn't checked or was true.
    # It other words, if ``except: ...`` wasn't entered.
    ...

So in short, it makes some sense as a fancy finally.

No, it depends on the design. In English, “else” doesn’t mean “if not”, the meaning is different. There is an idom “or else”, but even that has multiple meanings. And being similar to other programing languages is a design decision. That’s why I’m only using different Python statements as a reference.

except BaseException: would work, but I think it’s laziness or attempt on optimization. Funny enough, except Any: will raise an exception. “TypeError: catching classes that do not inherit from BaseException is not allowed”.

You should read again the paragraph that I wrote.

This is the strongest argument for changing try/else (and for/else): that people misunderstand them constantly.

But it’s absolutely NOT an argument for changing it in a way that is outright incorrect.

Have you heard of Chesterton’s Fence? Broadly speaking, it’s inadvisable to propose changes until you fully comprehend the status quo. It may well be that there is confusion, and that that confusion might merit a change, but giving incorrect analysis of the status quo will do nothing but undermine any proposal.

But… the other thing to note is exactly how many core devs have participated in this thread.

1 Like

But it’s absolutely NOT an argument for changing it in a way that is outright incorrect.

Oh yea, totally agreed!

I think the confusion in this thread is kinda the point in the first place, that’s my point. The incorrect proposals kinda makes the point even more in a way. Not because it makes the proposal stronger (because obviously it makes it weaker), but because it makes the need for some change more clear.

1 Like

That sounds like a docs issue to me.

1 Like

I disagree. The docs are quite clear. It’s just that language > docs. We can illustrate this by a hypothetical language where “if” means “else” and “else” means “try”. Such a language could be 100% correctly and well documented and it would still be horrible :stuck_out_tongue:

I’m not saying try/else and for/else are that bad, but they are not far from it.

I have explained for/else to the same (in my opinion very competent) co-worker several times over the course of months and they are surprised every time. Language intuitions trump documentation every time. If the language leads to an intuition, over time that intuition will override whatever docs they read. Unless they are a language lawyer like myself :stuck_out_tongue: (This assumes I can actually correctly describe try/else and for/else now, which I am now starting to doubt)

2 Likes

I like the OP’s idea of a soft keyword noexcept in place of else for a try block. It just makes the code that much more comprehensible to newcomers. But since we’re talking about introducing a new soft keyword anyway, how about making it even more readable with no except instead?

Similarly, for a for/while loop I suggest we can use no break in place of else:

while loop_condition:
    ...
    if break_condition:
        break
    ...
no break:
    ...
1 Like

As I said earlier, I think try/else is exactly that bad:.


Also, I used the word “replace” instead of “change” on purpose. Old code should work as it did. I never meant to propose change in how try/else work. Everything I say in h2 “Try statement” is in relation to:.

All phrases like “it should work like that” are in relation to this, and not a proposal. I’m sorry if someone misunderstood me. I thought it was clear.

1 Like

I naturally don’t like this. After all, it is in the opposite direction to my proposal that was referenced.

I personally prioritise consistency, functionality and terseness over readability.

While the only benefit of this proposal is readability, the costs are extensive:

  1. New keywords
  2. Backwards compatibility
  3. Syntax Changes
  4. Obstruction of other (possibly better) ideas of improvement in this area.

It is not to say that that I don’t care about readability, but it is a very rare occasion that it is a determining factor. Especially when it is changing existing code (as opposed to inception of a new one, where there are no backwards compatibility and other issues).

1 Like

Would not be good enough?

Since the point of this proposal is to use more natural-language-like keywords that can self-explain that the following block gets executed when no exception occurs or when no break takes place, no except and no break are clearer in meaning than not except and not break IMHO. The parser now allows soft keywords so I think we should take advantage of this capability to improve readability where it makes sense.

I would still consider not except and not break big improvements to else though, if introducing a new soft keyword for the sole purpose of improving readability is deemed too much.

We can retain the existing constructs while adding new soft keywords as plain equivalent alternatives. There will be no backwards compatibility issue since soft keywords are… soft.

1 Like