7

What is the most elegant way to repeat something after it caused an exception in python?

I have something like this [pseudo code as an example]:

try:
  do_some_database_stuff()
except DatabaseTimeoutException:
  reconnect_to_database()
  do_some_database_stuff() # just do it again

But imagine if I don't have a nice function but a lot of code instead. Duplicate code is not very nice.

So I think this is slightly better:

while True:
  try:
    do_some_database_stuff()
    break
  except DatabaseTimeoutException:
    reconnect_to_database()

That's good enough if the exception really fixes the problem. If not I need a counter to prevent an indefinite loop:

i = 0
while i < 5:
  try:
    do_some_database_stuff()
    break
  except DatabaseTimeoutException:
    reconnect_to_database()
    i += 1

But then I don't really know if it worked so it's also:

while i <= 5:
  try:
    do_some_database_stuff()
    break
  except DatabaseTimeoutException:
    if i != 5:
     reconnect_to_database()
    else:
      raise DatabaseTimeoutException
    i += 1

As you can see it starts to get very messy.

What is the most elegant way of expressing this logic?

  • try something
  • if it fails apply fix
  • try n more times including the fix
  • if it continues to fail give me an error to prevent a indefinite loop

2 Answers 2

6

You can use a "for-else" loop:

for ii in range(5):
    try:
        do_some_database_stuff()
        break
    except DatabaseTimeoutException:
        reconnect_to_database()
else:
    raise DatabaseTimeoutException

Or, without:

for ii in range(5):
    try:
        do_some_database_stuff()
        break
    except DatabaseTimeoutException:
        if ii == 4:
            raise
        reconnect_to_database()
Sign up to request clarification or add additional context in comments.

3 Comments

The top answer to that questions states "But anytime you see [a for-else] construct, a better alternative is...". I agree. Better to wrap the code block in a function and use return rather than break.
@Dunes: that's why I provided an alternative, for those who find the for-else a bit too much. I'm on the fence, personally, it's certainly a matter of taste.
The for-else version isn't equivalent as it'll run reconnect_to_database 5 times on 5 failures instead of 4.
1

I'm personally not a fan of the for-else construct. I don't think it's intutitive. First time I read it I thought it meant "do for loop (...), if iterable was empty then ...".

You should place your code inside a function. If do_some_database_stuff() completes successfully then you can use the return statement to return early from the function.

eg.

def try_to_do_some_database_stuff():
    for i in range(num_times):
        try:
            return do_some_database_stuff()
        except DatabaseTimeoutException:
            reconnect_to_database()
    raise DatabaseTimeoutException

If you find yourself using this construct quite a lot then you can make it more generic.

def try_to_do(func, catch, times=2, on_exception=None):
    for i in range(times):
        try:
            return func()
        except catch:
            if on_exception:
                on_exception()
    raise catch

try_to_do(do_some_database_stuff, catch=DatabaseTimeoutException, times=5, 
    on_exception=reconnect_to_database)

You can use functools.partial if you need to pass arguments to your functions.

2 Comments

This will run reconnect_to_database 5 times for 5 failures which seems a bit pointless.
It's copying the behaviour of the loops in the question. An additional i+1 < times in the if statement would suffice to change the behaviour to not respond to the last exception.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.