148

I have a shell script that loops through a text file containing URL:s that I want to visit and take screenshots of.

All this is done and simple. The script initializes a class that when run creates a screenshot of each site in the list. Some sites take a very, very long time to load, and some might not be loaded at all. So I want to wrap the screengrabber-function in a timeout script, making the function return False if it couldn't finish within 10 seconds.

I'm content with the simplest solution possible, maybe setting a asynchronous timer that will return False after 10 seconds no matter what actually happens inside the function?

Aaron Hall
  • 291,450
  • 75
  • 369
  • 312
Christoffer
  • 20,397
  • 16
  • 49
  • 75
  • 6
    For all lazy people, who love to use libraries instead of copy+pasting code snippets from StackOverflow: https://pypi.python.org/pypi/timeout-decorator – guettli May 02 '17 at 12:42

2 Answers2

244

The process for timing out an operations is described in the documentation for signal.

The basic idea is to use signal handlers to set an alarm for some time interval and raise an exception once that timer expires.

Note that this will only work on UNIX.

Here's an implementation that creates a decorator (save the following code as timeout.py).

from functools import wraps
import errno
import os
import signal

class TimeoutError(Exception):
    pass

def timeout(seconds=10, error_message=os.strerror(errno.ETIME)):
    def decorator(func):
        def _handle_timeout(signum, frame):
            raise TimeoutError(error_message)

        def wrapper(*args, **kwargs):
            signal.signal(signal.SIGALRM, _handle_timeout)
            signal.alarm(seconds)
            try:
                result = func(*args, **kwargs)
            finally:
                signal.alarm(0)
            return result

        return wraps(func)(wrapper)

    return decorator

This creates a decorator called @timeout that can be applied to any long running functions.

So, in your application code, you can use the decorator like so:

from timeout import timeout

# Timeout a long running function with the default expiry of 10 seconds.
@timeout
def long_running_function1():
    ...

# Timeout after 5 seconds
@timeout(5)
def long_running_function2():
    ...

# Timeout after 30 seconds, with the error "Connection timed out"
@timeout(30, os.strerror(errno.ETIMEDOUT))
def long_running_function3():
    ...
Danra
  • 8,833
  • 4
  • 49
  • 113
David Narayan
  • 4,780
  • 2
  • 19
  • 6
  • 79
    Beware that this is not thread-safe: if you're using multithreading, the signal will get caught by a random thread. For single-threaded programs though, this is the easiest solution. – Wim Feb 17 '10 at 17:03
  • 3
    Nice. Also, it is recommended to decorate the function `wrapper` with `@functools.wraps(func)` – shx2 Oct 31 '13 at 19:58
  • 8
    FYI, there are missing parens after the first "@timeout". It should read `@timeout() def ...`. – ron rothman Mar 16 '14 at 22:28
  • 4
    @wim I think it can only be used in main thread, because if you use it in worker thread, it will raise 'ValueError: signal only works in main thread'. – flycee Apr 13 '15 at 12:26
  • 1
    Could also use https://docs.python.org/3/library/signal.html#signal.setitimer to allow half second timeouts. – Aurélien Ooms Nov 18 '15 at 16:36
  • 2
    @Wim Could you elaborate on which part is not thread-safe (I imagine something to do with `signal`) and what side-effects are to be expected ? Also, would you happen to have a threadsafe version ? – Bertrand Caron Apr 04 '16 at 23:17
  • This works great when the function you're decorating doesnt take any arguments. What changes would allow the decorator to be applied to say: def long_running_function4(my_arg): – Swoop Oct 26 '16 at 00:58
  • 1
    Seems the answer to my question above would be to add empty parenthesis to the decorator as such: @timeout() – Swoop Oct 26 '16 at 14:40
  • 1
    This is usually working for me, but it occasionally gets stuck on a Jpype call that never returns. Can I do something more forceful like a SIGKILL after 10 seconds? – sudo Jan 28 '17 at 17:14
  • 6
    What would be a viable alternative for this, when using threads? – SaAtomic Mar 08 '17 at 06:44
  • how would you use this inside a method in another class? – lightbox142 Jan 05 '21 at 20:01
173

I rewrote David's answer using the with statement, it allows you do do this:

with timeout(seconds=3):
    time.sleep(4)

Which will raise a TimeoutError.

The code is still using signal and thus UNIX only:

import signal

class timeout:
    def __init__(self, seconds=1, error_message='Timeout'):
        self.seconds = seconds
        self.error_message = error_message
    def handle_timeout(self, signum, frame):
        raise TimeoutError(self.error_message)
    def __enter__(self):
        signal.signal(signal.SIGALRM, self.handle_timeout)
        signal.alarm(self.seconds)
    def __exit__(self, type, value, traceback):
        signal.alarm(0)
vmarquet
  • 1,747
  • 2
  • 19
  • 33
Thomas Ahle
  • 28,005
  • 19
  • 77
  • 105
  • 9
    Python < v3 does not have a TimeoutError. But one can very easily write one own class with like explained here: http://stackoverflow.com/a/1319675/380038 – Framester Oct 02 '14 at 09:17
  • 5
    You could easily add in a decorator `@timeout.timeout` as a static method to this. Then, you could easily choose between a decorator or a `with` statement. – Kevin Oct 15 '15 at 18:09
  • 9
    Interesting to note that if inside the `with Timeout(t)` context any error is raised, the `__exit__` is still called, avoiding, thus, any complication caused by `TimeOutError` being raised instead of the real error. This is a very lovable solution. – lucastamoios Oct 30 '16 at 13:38
  • 1
    This is giving me an error if I try to give it a non-integer amount of seconds. Is there a way to make it so it works for floats? – Nick Crews Dec 18 '16 at 02:11
  • 1
    @Nick what about https://docs.python.org/3/library/signal.html#signal.setitimer – Thomas Ahle Dec 18 '16 at 09:12
  • 17
    Can someone recommend a viable solution that, like this, that works in threads? – SaAtomic Mar 08 '17 at 06:46
  • 3
    @Nick Some time ago I created version of timeout decorator, that works with floats - http://stackoverflow.com/questions/11901328/how-to-timeout-function-in-python-timeout-less-than-a-second/11901541#11901541 – Jendas Mar 13 '17 at 16:40
  • 3
    This does not work if the function is catching generic exception, ` def bad_func(): try: time.sleep(100) except Exception: pass ` – Brij Jul 06 '17 at 08:27
  • 1
    Only issue is: `signal only works in main thread` – User Mar 14 '19 at 02:56
  • 1
    I suggest using [`signal.settimer`](https://docs.python.org/3/library/signal.html#signal.setitimer) over `signal.alarm` for the added resolution. With this implementation shortest timeout is 1 second. – szdrnja Feb 17 '20 at 15:31