Ferdinand Keil's

electronic notes

Okt 12, 2021

Breaking up Exceptions

These days I stumbled upon a weird problem where a Python script of mine would silently fail to save some data to a file. Turns out you can suppress exceptions in a loop with break 😧.

Let me start by outlining what I was trying to achieve: I have a process where data is received and the saved to a file. Sometimes the retrieval fails so I start it again for certain number of retries. The failure is signaled by a certain exception which in turn is caught using a try block. However, if an unexpected exception is raised it should break the process and propagate up.

This is a minimal example of the code that was supposed to do this:

class NoSurprise(Exception):
    pass

retries = 3

for i in range(retries):
    try:
        raise NoSurprise # <- well that's no surprise
    except NoSurprise:
        print('try again')
        pass
    finally:
        print('done')
        break

If the expected exception NoSurprise is caught the loop will try again and up to three times. Running the code we get the output try again three times which is the desired behavior.

Now let's try again with an unexpected exception.

for i in range(retries):
    try:
        raise Exception # <- this we don't expect
    except NoSurprise:
        print('try again')
        pass
    finally:
        print('done')
        break

Running this just outputs done. Well, that's no good!

Let's go over what happens here. The exception is raised, but it doesn't fit the except statement, so it is skipped The finally block is run always, regardless what happened earlier. So it will happily print done and then break the loop. But what happened to the exception? Well, once the loop is broken the exception is gone and the rest of the code runs happily.

This is the fix for this code snippet.

for i in range(retries):
    try:
        raise Exception # <- this we don't expect
    except NoSurprise:
        print('try again')
        pass
    else:
        print('done')
        break

All I did was replace finally with else. What's the difference, I hear you ask. As I wrote earlier, the finally block will get executed no matter what. The else block on the other hand will only be executed if no exception was raised in the try block [1]. Running this piece of code Python informs us that an exception was raised an prints a traceback, which is what I wanted in the first place ☺️.

Python has very powerful constructs, but as they say ...

[1]For a authoritative source please refer to the Python docs: https://docs.python.org/3/tutorial/errors.html#defining-clean-up-actions