In the last post of my syntactic sugar series, I showed how you can get away with not having
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
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
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
A note about
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
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
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.