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:
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:
we can transform it into:
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.