Unravelling `finally` and `else` from `try` statements

In the last post of my syntactic sugar series, I showed how you can get away with not having elif and else clauses on an if statement. It turns out you can use the same trick to help get rid of else clauses on try statements. And then there's another trick we can use to get rid of finally clauses, making it so you only need try and except.

else

In the post on else clauses in if statements, we used a variable to track whether any other clauses of the overall if statement were executed. Since else clauses for try statements only execute if no exception is raised, we can record whether execution reaches the end of the try clause, signalling that no exception was raised. Take a simple example:

try:
    A
except:
    B
else:
    C
Example try statement with an else clause

We can mark whether A executes fully or not to control whether the else clause should execute.

_A_finished = False
try:
    A
    _A_finished = True
except:
    B
if _A_finished:
    C

finally

In contrast to else, we want finally to always execute. That makes our biggest concern being not whether some other code executed but making sure we execute the finally clause once, every time. The challenge then is how to run the code from the finally clause no matter what exception is raised as well as if no exception is raised?

Tackling the case of when an exception is raised, we should be able to wrap the entire try statement in an outer try statment with a catch-all except BaseException clause that contains the finally clause's code. We can then use a bare raise to let the exception we initially caught to continue to propagate.

For the "no exception raised" case, we can just copy the code after our added try statement. That works because we insert that raise statement in our except clause to make sure that exceptions keeping going.

That's a lot of words, so let's move on to some code to try and help make sense of it all. With the following example:

try:
    A
except Exception:
    B
finally:
    C
Example try statement with a finally clause

we can transform it into:

try:
    try:
        A
    except Exception:
        B
except BaseException:
    C
    raise
C
Unravelling of the finally example

As you can see we leave the initial try statement alone except for removing the finally clause. We then duplicate the code in the finally clause so it will be run both in the exception-raised case and the no-exception case.

And with that, we can simply try statements down to just that and except clauses!

A note about return

As someone was nice enough to point out to me after the first posting of this blog post, return makes everything I say above more complicated. Because Python can easily capture when a return occurs and still guarantee that else and finally clauses execute, return isn't really something you think about. But when you're trying to guarantee execution of stuff, it becomes ... challenging 😉.

There's a couple of ways to deal with return, but they all involve delaying the execution of return somehow. One is to store what you want to return, set a flag to stop executing other code, fall through to the code you want to make sure runs, and then return. The other is to copy the code you want to make sure runs before every return statement.

def f(n):
    """Original"""
    try:
        return 1/n
    except Exception:
        print('except')
    finally:
        print('finally')


def f(n):
    """Saving the return to the end."""
    try:
        try:
            _return = 1/n
        except Exception:
            print('except')
    except BaseException:
        print('finally')
        raise
    print('finally')
    return _return


def f(n):
    """Inlining `finally`."""
    try:
        try:
            _return = 1/n
            print('finally')
            return _return
        except Exception:
            print('except')
    except BaseException:
        print('finally')
        raise

Both approaches lead to the same, correct result, they just vary in the approach and thus which one you consider less complicated.