Generalize `elif-else-finally` to all compound statements

1. Proposal

Currently, each compound statement has a subset of clauses, but neither of them has a full set (union).

I think by now, all clauses have proven to be useful python constructs and it might be worthwhile considering sharing the full set among all of the compound statements.

<MAIN STATEMENT CLAUSE(S)>
elif cond2:# *
    expr3()
else:      # ?
    expr4()
finally:   # ?
    expr5()

# where <MAIN STATEMENT CLAUSE(S)> are one of the following:
# 1. Conditional
if cond1:
    expr1()
# 2.
try: Try-except
    expr1()
except exc:# * (if ommitted, only single `finally` is allowed)
    expr2()
# 3. While statement
while cond:
    expr1()
# 4. Compound iterative
for target in iter:
    expr1()

2. Use Cases

Requests from users:

CPython code base:

  1. 'try\:.*else\:\s*if.*' --nlines=10 | wc -l
    1.1 946 / 11 ~ 86
    1.2 With site-packages: 4774 / 11 ~ 434
    1.3 inspect.py: 99 / 11 ~ 9
  2. czg ''try\:\s*if .*\:.*finally.*'' . --nlines=10 | wc -l
    2.1 649 / 11 ~ 59
    2.2 With site-packages: 2179 / 11 ~ 198
  3. czg 'try\:\s*for .*\:.*\:.*finally.*' . -cG --nlines=10 | wc -l
    3.1 253 / 11 ~ 23
    3.2 With site-packages: 625 / 11 ~ 57

It most likely has false positives, but overestimation in general is not very likely. I looked into inspect and all cases are valid. Also, this only tests for 4 cases and only registers hits that span maximum 10 lines.

For CPython repo (compared to things I have looked at before) these are quite big numbers.

One example of use case research of CPython vs Github: \.count\(.*\).*[><=]

  • cpython: 38
  • github: 432K
  • Which is ~ 1 / 11000.

Multiline search on Github is not provided (at least as far as I know), so it is only a guess that the ratio could be similar.

A concrete case from each of the searches:

# asyncio/unix_events.py:548:0
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)
    else:
        if self._loop.get_debug():
            logger.info("%r was closed by peer", self)
        ...
-->
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)
elif self._loop.get_debug():
    logger.info("%r was closed by peer", self)

# logging/handlers.py:1406:0
try:
    if self.flushOnClose:
        self.flush()
finally:
    with self.lock:
        self.target = None
        BufferingHandler.close(self)
-->
if self.flushOnClose:
    self.flush()
finally:
    with self.lock:
        self.target = None
        BufferingHandler.close(self)

# asyncio/proactor_events.py:748
try:
    while True:
        blocksize = min(end_pos - offset, blocksize)
        if blocksize <= 0:
            return total_sent
        await self._proactor.sendfile(sock, file, offset, blocksize)
        offset += blocksize
        total_sent += blocksize
finally:
    if total_sent > 0:
        file.seek(offset)
-->
while True:
    blocksize = min(end_pos - offset, blocksize)
    if blocksize <= 0:
        return total_sent
    await self._proactor.sendfile(sock, file, offset, blocksize)
    offset += blocksize
    total_sent += blocksize
finally:
    if total_sent > 0:
        file.seek(offset)

3. Benefits

  1. Learning - once one statement is learned, only 1 clause is left to learn for another one. try-except would have either 2+ clauses or special case of try-finally (as it is now). It might provide a bit steeper learning curve at the start, but for the longer term benefit.
  2. More readable code. It would provide code simplifications and would eliminate a portion of nested compound statements.
  3. General consistency, thus better code. E.g. frameworks that build compound statement ASTs could re-use code and simplify the logic.

It is backwards compatible (at least it seems so).

Would be very interested to get some feedback for this

1 Like

I don’t think a grep is sufficient evidence that this is useful. Can you show examples of the diff with existing code, and explain why it’s better?

The single except already has break at its end. You could just dedent the if-elif-else and remove the else: line belonging to the try statement. I suspect that many examples allow similar transformations that would increase clarity.


For loops, would elif be executed more than once? The answer is probably not, but I don’t think this is obvious enough to warrant adding the possibility of this confusion. People already find the semantics of else confusing, I don’t think adding more confusion is a good idea.


I think the semantics of finally in if-elif chains would be too confusing. Would it be executed always? Only when at least one of the blocks is executed? What about break/continue/return? What about a raised exception? (if in case of exception, does this mean that if True: ... finally: ... and try: ... finally: ... are equivalent?)

1 Like

Added 1 example from each case.

The reasons why are the same for majority of cases and I have listed them at the end.

Nothing magical here, all same rules prevail and are transferred from existing statements to new ones appropriately. So far I have not found any ambiguities.

In short, cleaner and more convenient code, and consistency. Thus, better python experience.

Good catch. Added another example in place of it. It is very possible that some of them (maybe even a big part) can be simplified without this.

No, it should be seen as extension of else, not for. If thinking in this way, I don’t think it adds much more complexity on top of else

Finally would follow exact same rules as they are now. I.e. 8. Compound statements — Python 3.12.4 documentation

So far it seems they can be extrapolated without any complications. At least those are not yet obvious to me.

Yes, they would be.

However, if would allow adding if-else statements, while try-finally would be left a special cased construct same as it is now.

Don’t repeatedly edit your posts, it makes the conversation hard to follow and people reading this forum via email won’t get the updates. Just make a new post.

2 Likes

I generally do have this bad habit on which I am improving.

However, in this case, I thought it is not a big problem if I update main thread given that there is a reply indicating that?

Or do I need to leave an example which is just plain wrong in the main thread as well as in replies?

Except that it’s not consistent. At least, not if it has the semantics given in that SO post. That implies that if/elif/elif/elif/finally does NOT run the finally if none if the ifs match, which is quite different from what happens in a try/except/finally. Also, does it or doesn’t it handle exceptions? If it does, it’s even weirder that it doesn’t handle the else, and if it doesn’t, then it’s inconsistent with try/finally.

Huge -1 from me. This is attempting to solve a problem that might maybe exist [1], but using tools that are completely wrong for it.


  1. the example in SO has several solutions, none of which is really ideal ↩︎

1 Like

I’d probably just leave a note in the original post that something was wrong, and write new content in new posts.

I think the main issue is that new content won’t be seen by most people (including web users who aren’t going to check posts they’ve already read).

1 Like

My proposal deviates from SO post. It is similar, thus I have missed that it is not talking about the same thing I am (I have skipped to replies and got the wrong idea as they complied to what I am proposing).

finally does handle exceptions and is unequivocally called regardless of what happened before. Same as in current try-[except]-finally.

SO post is misplaced here, best to disregard it altogether.

Then, if I’m understanding you correctly, this is just removing an indentation level from a try/finally with an if/elif inside it? Not really adding much.

Exactly. This time I am not attempting to solve any specific problem. Everything can be done with existing tools and there is no real issue here.

This is mainly to improve general consistency and readability. With better consistency, it is then easier to design on top / in parallel.

E.g. PEP 463 – Exception-catching expressions | peps.python.org. When consistency is lacking it is difficult to design optimal solution. Meaning that if to design one expression for one compound statement neglecting others, then there is a risk that once the next one needs to be implemented something will not fit and structure of expressions will differ for different compound statements, which leads to sub-optimal learning curves.

Of course, to standardise, enough input and reassurances are needed that this is the right way to do this, does it make sense, etc.

This one has been sitting with me for a year now. So thought maybe it is time to see what others think about it.

Also, this could lead to some small optimizations further down the line.

But this is just my intuition for now and is secondary.

I might be willing to take this up with with limited probability of success. If I felt that there is at least a decent chance I might use this as a motivation to understand Python parts that I wanted for a while now. 50% would be good enough. Maybe could do with 30-40%.

Figuring out if this has any serious obstacles that make this a complete no-go is obviously a prerequisite.

Then, do people like it at all? Does it feel natural/pythonic? E.g.:

while True:
    a += do_stuff()
elif a == 0:
    do_something_else()
else:
    do_something_different()
finally:
    cleanup_in_any_case()

Or:

try:
    a = something()
except SomeError:
    do_something_else()
elif a is None:
    raise NotImplementedError
else:
    return a
finally:
    do_cleanup()

In terms of applicability, there are plenty of cases where this could be used instead of nested statements. But I think the major factors are whether community likes it and whether it makes sense from different points of view (if this has no other major issues).

It seems like your suggestion boils down to removing a level of indentation from fairly specific combinations of try + if/while/for and while/for ... else + if blocks. Is that really worth introducing a new syntax feature for?

My mental model of try ... finally isn’t that the finally is a block that can be added below another and tacks some code to the end of its control flow. Rather, try is the fundamental block here and the finally doesn’t really make sense on its own. Given how this feature is usually taught and how it doesn’t really make sense to add finally after arbitrary blocks like classes or functions, I suspect this matches most people’s understanding.

I can kind of see the idea with elif after loops, but since most people consider the far simpler else in those places to already be a mistake, I think this is a non-starter. Yes, you can define semantics for this and it is consistent but consistency for its own sake isn’t really worth pursuing. There isn’t a big gain from this feature and it just further complicates an already little understood part of the language.

Do you have other advantages of this feature in mind that I’m missing? Ideally there would be some use cases that go beyond situations where you can use this feature and into places where using this feature provides a genuine improvement to the code.

1 Like

So far I have only identified straight-forward simplifications of statements. After all, this does not seem to add anything new so far.

To me, simplifying nested statements in addition to consistency across different ones, does seem to provide a genuine improvement. But I see what you mean, it does not go beyond that. E.g. it does not offer any DRY improvements (apart from not repeating the statement syntax itself), or anything similar.

I tend to explore and cover such things for fairly long term benefit when there is enough clarity on how to go about it. Consistency does seem to pay off in most cases that I identify, even if it takes time to realise them. And this one seems to me like a potential candidate for such.

Although I appreciate that this might not make sense from POVs of others or can be objectively not worth it, or even be a plain bad idea.

But IS it consistency? You’re adding just a small part of exception handling code to other statements, instead of letting exception handling be done with try. I don’t see that as an improvement in consistency.

1 Like

To take this apart:

Addition of elif to all statements where it is missing (applies to all):

while True:
    ...
else if cond:
    ...
# identical to
while True:
    ...
else:
    if cond:
        ...

Addition of finally to all statements where it is not not (applies to all):

while True:
    ...
finally:
    ...
# Equivalent to
try:
    while True:
        ...
finally:
    ...

So in terms of functionality this does not add anything.

To be precise: “Consistency across compound statement additional clauses”

To be consistent is to be predictable.

If I know additional clauses of if clause, and the same additional clauses are valid to other statements, then there is consistency.

This has an impact on the user - learning curves are more efficient when there are good consistent patterns. To me it took a fair bit of time to work out and get used to all the statement clauses and their intricacies. It would have been much easier and faster if there was a better pattern to follow. Well, at least this is what helps me most in learning new things.

But also, it has an impact on dependent parts as they can plug-in easier to full functionality of the whole group. In this case, as far as I can see at the moment (and I still don’t see a lot of things) this has an impact on parser and AST.

I think one big factor that led me to considering this is the presence of else statement in for and while. If each statement was as simple as it can be with only necessary overlap, then I don’t think I would be looking at this. But as some overlap has already happened, things are somewhere in the middle between being largely independent and consistent.

else presence in statements other than if-else means that there is no turning back.

So in a sense things seem to be in sort of a limbo now, thus it is tempting for me to explore what it would mean to go in a direction of making them more alike.

Also, the fact that else can currently be confusing in non if-else statements does not necessarily mean that further proposed additions will make it even more confusing. On the contrary, completing the pattern might lead to better clarity/more intuitive user experience.

I can sort of imagine using for...elif...else in some rare cases. But finally just doesn’t make sense outside of a try/except block.

1 Like