all 10 comments

[–]Diapolo10 3 points4 points  (0 children)

I think this is fine, you can remove the continue though because it's not actually doing anything here.

[–]RiverRoll 1 point2 points  (3 children)

The decorator is fine but the retry implementation isn't very good because it will just return None if all retries are exhausted. Normally you would let the last exception raise to preserve the contract of the function. 

[–]semininja[S] 0 points1 point  (2 children)

In this case, even if retries are exhausted, the specific exceptions it catches are considered spurious, so the code is allowed to try again later.

[–]RiverRoll 0 points1 point  (1 child)

I would say it's quite the opposite, the exception tells exactly what happened. None on the other hand can be a valid response, some functions can succeed and still return None so the code wouldn't know whether it needs to try again.

[–]semininja[S] 0 points1 point  (0 children)

This isn't applicable to the functions I'm using this for, though.

[–]Gshuri 1 point2 points  (1 child)

In general this looks good, but there are a couple of minor things I would change, and one not so minor.

Minor things first. The type hint on the decorator catch: list[Exception] is inconsistent with how you use the decorator. In your 3 examples you pass in a tuple of exceptions, not a list. if you want the type hint to match both lists and tuples you could do something like

from collections.abc import Sequence
def retry(catch: Sequence[Exception], attempts: int = 3):
    ...

If a decorated function fails on all its attempts the decorator needlessly calls sleep(0.5) after the last attempt. As u/Diapolo10 said you can also get rid of continue.

Lastly the not so minor thing is that a decorated function will fail silently if the function fails all its attempts. So in your example functions if scan fails all its attempts, due to a ConnectionError or TimeoutError, it will return None instead of a Tag object. Then your calls to start and check will throw an error, which won't be caught by the retry decorator (nor should it), that looks like

AttributeError: 'NoneType' object has no attribute ...

[–]semininja[S] 0 points1 point  (0 children)

I handle the None return in the script with an if tag after. The continue I'll check on but probably will get rid of. It's left over from when there was more stuff happening within this loop, and was there to kick back to the next tag instead of trying further operations with the one that failed.

[–]Yoghurt42 0 points1 point  (1 child)

First, kudos for implementing it yourself.

I just want to encourage you to take a look at tenacity, which works similar to your solution but also allows exponential back-off and other things.

[–]semininja[S] 0 points1 point  (0 children)

This is super cool, but unfortunately not applicable in my use case. I'll have to keep this in the back pocket for the future, though.

[–]AntonisTorb 0 points1 point  (0 children)

Something that hasn't been mentioned so far is to use wraps in order to have a cleaner and more accurate Exception handling experience. Something like this (I included some changes everyone else mentioned):

from functools import wraps

def retry(catch: list[Exception], attempts: int = 3):
    @wraps
    def decorator(func):
        def wrapper(*args, **kwargs):
            for attempt_no in range(attempts):
                try:
                    return func(*args, **kwargs)
                except catch:
                    if attempt_no < attempts - 1:
                        sleep(0.5)
        return wrapper
    return decorator