`while-elif-else`, `for-elif-else`, `try-except-elif-else`

Generalize `elif-else-finally` to all compound statements - #23 by xitop has suggested a bigger change including finally. However, the feedback revealed that finally should ideally be left out.

So apologies for creating very similar thread, but I think clean new thread to get feedback on narrowed down idea is more productive and time saving for everyone.

1. Proposal

Add optional elif clauses to statements that support else, but elif is not allowed. These are the following 3:

  1. for statement.
for target in iter:
    ...
elif condition: # 0 or more
    ...
else:           # 0 or 1
    ...
  1. while statement
while condition0:
    ...
elif condition1: # 0 or more
    ...
else:            # 0 or 1
    ...
  1. try-except statement
try:
    ...
except Exception: # 1 or more
    ...
elif condition:   # 0 or more
    ...
else:             # 0 or 1
    ...
finally:          # 0 or 1
    ...

In essence, this would allow flattening out nested compound statements.

while condition0:
    ...
else:
    if condition1:
        ...

becomes:

while condition0:
    ...
elif condition1:
    ...

And:

while condition0:
    ...
else:
    if condition1:
        ...
    else:
        ...

becomes:

while condition0:
    ...
elif condition1:
    ...
else:
    ...

The same is true for all 3 compound statements.

2. Use cases

Cases in CPython repo and few accompanying links:

  1. for <target> <iter>:\n<indented-lines>else:\nif - 81
    1.1 multiprocessing/pool.py:534
    1.2 zoneinfo/_zoneinfo.py:249
    1.3 stat.py:167
  2. while <cond>:\n<indented-lines>else:\nif - 2
    2.1 textwrap.py:320
    2.2 tkinter/dnd.py:156
  3. try:\n<indented-lines>else:\nif - 10
    3.1 uuid.py:430
    3.2 socket.py:388
    3.3 ssl.py:336

Cpython repo with site-packages from .venv.

  1. try-else-if - 407
  2. for-else-if - 114
  3. while-else-if - 28

Notes:

  1. These numbers contain little to no false positives.
  2. Non-trivial portion of cases is not captured. E.g. blank-lines are not allowed. (If I add conditional new line re search never finishes)
  3. These numbers are fairly significant compared to code-usage searches I have done in the past.

Random concrete example: asyncio/unix_events.py:548

try:
    data = os.read(self._fileno, self.max_size)
except (BlockingIOError, InterruptedError):
    pass
except OSError as exc:
    self._fatal_error(exc, 'Fatal read error on pipe transport')
else:
    if data:
        self._protocol.data_received(data)

Would become:

try:
    data = os.read(self._fileno, self.max_size)
except (BlockingIOError, InterruptedError):
    pass
except OSError as exc:
    self._fatal_error(exc, 'Fatal read error on pipe transport')
elif data:
    self._protocol.data_received(data)

The proposal is very simple in concept and implementation.

So could this be worth implementing?

1 Like

The problem I have with adding else like this is that I have no intuition about what it means. Or experience of any other language that does this.

Every time i see else with for i have to read the docs to remind myself what it does, and if i have the chance to i will refactor to eliminate it.

7 Likes

To me it is actually quite simple. There are few ways to think about it.

My personal choice is however counter-intuitive with respect to naming:

  • “If loop finished successfully, continue with the next clause of the statement”

If one wants to use else as part of the thinking:

  • “if loop was broken - do nothing, else: do something”

2 things:

  1. Is there any chance that this will be eliminated?
    1.1. If yes: then this needs to be discussed. If else was not already implemented, I would surely not be suggesting it.
    1.2. if no: in other words, it is going to stay, period. Then is it better to complement it with elif for more flexibility or not?
  2. Although I agree that it can be a bit of a mind-bender at first, I got used to it over time. I use it in straight forward manner whenever appropriate and also it is my favourite approach to break out of multiple nested loops.

else is probably not going anywhere. But at the same time, it is a very niche and confusing part of syntax. elif would be even more confusing, even more niche, even more surprising and hard to interpret, and it does not seem like a significant improvement (it’s just removing a single level of indentation).

7 Likes

When I see the else (and also the elif) I see them as part of the if that is enclosed in the condition of the for, while, try (if no exception) and the if itself.

The only conflicting idea that crosses my mind after that is whether they are inside inside the scope of the compound statement or not. For example, whether

while A:
  a
elif B:
  b
else:
  c

is

while True
  if A:
    a
    continue
  elif B:
    b
    continue
  else:
    c
    break

or the other meaning. My own mnemonic is to use the indentation to remind myself that the else (and perhaps later the elif) is not inside.

I think it is nice for those expressions to have similar structures.

I find the use of else clauses in loops unintuitive because they depend on the absence of break statements. If there is no break, the else block is executed. If it were called no_break, it would make more sense.

The Python documentation should emphasize the relationship between loops and else blocks. The else block only makes sense if there is a possibility of breaking out of the loop. The fact that Python allows writing a for/else loop without using break makes it error-prone and confusing.

However, I find that using elif within the loop does not align well with the semantics of for+break. There are only two possibilities: we either break out of the loop or let the loop finish.

2 Likes

And when we let the loop finish, it enters else if it is present.

The question is whether it is worth adding elif to this path.

(You could have provided links for the mentioned use cases.)

I looked at some of the use cases and I only saw guards. There can be many guards in a single else clause, which the proposed syntax could not handle.

for loop:
    for loop:
        if cond:
            break

    else:
        if not guard:
            code
            # continue

        if not guard_2:
            code
            continue
        
        code

The situation is that the usage of these else clauses is very limited. I haven’t encountered them in practice, and if I do, I often have to refer to Python documentation. Then, I add comments like # no break or # no exception, which disrupts the flow of code reading. I am opposed to the else clause, and elif wouldn’t improve it.

Which ones?

If you are referring to double breakout, then it has to do with already existing else and little to do with this proposal.

This proposal is simply to provide elif that eliminates the need for nested statements in some cases.

Btw, your example can be simplified to:

for loop:
    for loop:
        if cond:
            break
    elif not guard:
        code
        code_from_the_bottom
    elif not guard_2:
        code

But outer loop has no significance in this example. To break out from it you need another break. I.e.

for loop:
    for loop:
        if cond:
            break
    elif not guard:
        code
    elif not guard_2:
        code
    break

YMMV. I find them idiomatic. If there’s a break in a for loop, then having an else clause is natural. I’ll even use else on a for loop with return; static checkers yell at me because it’s unnecessary, but I do it anyway because it expresses intent well.

But you’re in good company for disliking for/else. I think even GvR has once said that he thinks the construct was a mistake.

Even as someone who likes for/else, I wouldn’t want for/elif. It’s a marginal feature that can’t bear further complication.

2 Likes

The reason I am on it is because I think it might be possible to make it to be a lesser mistake than it is. See:

To me personally, it would simplify things, but ofc my personal case could be an exception.

elif was introduced because a staircase code like the following is very common. Especially if there is no match statement.

if cond1:
    ...
else:
    if cond2:
        ...
    else:
        if cond3:
            ...
        else:
            if cond4:
                ...
            else:
                ...

You can have tens of sequential checks. So merging lese and if was essential.

Nothing of this exist in other cases for else. else after a loop is extremally rare, and else after a loop with a single if statement in the else block is a small fraction of these cases. else after try is also used in a fraction of try statements. In any case, elif would save only one indentastion level in few rare cases. This is too litle to justify a grammar addition.

2 Likes

Yes, one indentation level per statement. However, there are cases such as:

try:
except:
else:
    if:
        try:
        except:
        else:
            if:

where it would be 1 per statement. Given 79-character length line standard in python this could be useful.

It depends what is considered to be “rare”. Aggregate number in standard library 93.

From experience ration of standard library to github, which can be (at least for one case I have checked) 1/10_000, this could be 1M use cases in github code.

Which is much more than use case numbers of some proposals that have reached implementation stage.

This highlights the LACK of consistency in your proposal. Had the condition not been there, you wouldn’t be able to do this:

try:
except:
eltry:
except:
elif:

Why combine try/else with the if inside it, but not others? It makes no sense.

But those cases that I am talking about were non-syntactic.

It is difficult to find analogous case to compare use cases.

One way to look at this is current use cases of try-else, for-else and while-else:
111 + 64 + 276 = 1451

Which is roughly 15 times more.

However, this is a comparatively trivial extension, not a new construct. And also, number above includes the impact of the fact that it has been implemented for a long time that resulted in new use-cases.

No it DOESN’T. I have specifically defined by what I mean with consistency and this does not violate it.

Your indicated change is much more complex.

It is an extra simplification which is orthogonal to what I am proposing here. In other words, the addition of my proposal does not obstruct implementing your idea.

Neither extension of:

try:
except:
else:
except:
else:

would obstruct what I am proposing here.

Where else goes elif can be used before it. Thus it is consistent across statements. There is no need to try to portray that I am saying something else.

Having that said, what you have noticed is great.

A combination of what I am proposing and extension you have identified would eliminate multiple levels of indentation from try-except-else statements in multiplicative manner as opposed to additive.

“When I use a word, it means just what I choose it to mean - neither more nor less.” – Humpty Dumpty

If you’re going to redefine the word “consistency” to mean whatever you want it to, I don’t think there’s much point discussing this with you. At very best, all you’re doing is playing with syntax rules and ignoring actual practicalities.

Have a read through CPython’s grammar files. There are actually a lot of weird oddities and duplications in order to make things “work intuitively”. It is FAR more important for things to make sense in a practical and usable way than for the grammar to appear purely logical.

2 Likes

I am having trouble understanding what you are not agreeing with.

Could you please be more specific?

“To be consistent is to be predictable. Thus, if one compound statement supports elif-else clauses and the next one also supports elif-else clauses, the behaviour is predictable. It would be less predictable if one would support it, but the other would not, or only a part of it.”

What exactly in this do you disagree with?

What meaning did I re-define?