Python: Make Time Delay (Sleep) for Code Execution

Introduction

Code Delaying (also known as sleeping) is exactly what the name implies, the delaying of code execution for some amount of time. The most common need for code delaying is when we're waiting for some other process to finish, so that we can work with the result of that process. In multi-threaded systems, a thread might want to wait for another thread to finish an operation, to continue working with that result.

Another example could be lessening the strain on a server we're working with. For example, while web scraping (ethically), and following the ToS of the website in question, abiding by the robots.txt file - you might very well want to delay the execution of each request so as to not overwhelm the resources of the server.

Many requests, fired in rapid succession can, depending on the server in question, quickly take up all of the free connections and effectively become a DoS Attack. To allow for breathing space, as well as to make sure we don't negatively impact either the users of the website or the website itself - we'd limit the number of requests sent by delaying each one.

A student, waiting for exam results might furiously refresh their school's website, waiting for news. Alternatively, they might write a script that checks if the website has anything new on it. In a sense, code delay can technically become code scheduling with a valid loop and termination condition - assuming that the delay mechanism in place isn't blocking.

In this article, we'll take a look at how to delay code execution in Python - also known as sleeping.

Delaying Code with time.sleep()

One of the most common solutions to the problem is the sleep() function of the built-in time module. It accepts the number of seconds you'd like the process to sleep for - unlike many other languages that are based in milliseconds:

import datetime
import time

print(datetime.datetime.now().time())
time.sleep(5)
print(datetime.datetime.now().time())

This results in:

14:33:55.282626
14:34:00.287661

Quite clearly, we can see a 5s delay between the two print() statements, with a fairly high precision - down to the second decimal place. If you'd like to sleep for less than 1 second, you can easily pass non-whole numbers as well:

print(datetime.datetime.now().time())
time.sleep(0.25)
print(datetime.datetime.now().time())
14:46:16.198404
14:46:16.448840
print(datetime.datetime.now().time())
time.sleep(1.28)
print(datetime.datetime.now().time())
14:46:16.448911
14:46:17.730291

Though, keep in mind that with 2 decimal places, the sleep duration might not be exactly on spot, especially since it's hard to test, given the fact that the print() statements take some (variable) time to execute as well.

However, there's one major downside to the time.sleep() function, very noticeable in multi-threaded environments.

time.sleep() is blocking.

It seizes up the thread it's on and blocks it for the duration of the sleep. This makes it unfit for longer waiting times, as it clogs up the thread of the processor during that time period. Additionally, this make it unfit for Asynchronous and Reactive Applications, which oftentimes require real-time data and feedback.

Another thing to note about time.sleep() is the fact that you can't stop it. Once it starts, you can't externally cancel it without terminating the entire program or if you cause the sleep() method itself to throw an exception, which would halt it.

Asynchronous and Reactive Programming

Asynchronous Programming revolves around parallel execution - where a task can be executed and finished independent of the main flow.

In Synchronous Programming - if a Function A calls Function B, it stops execution until Function B finishes execution, after which Function A can resume.

In Asynchronous Programming - if a Function A calls Function B, regardless of its dependence of the result from Function B, both can execute at the same time, and if need be, wait for the other one to finish to utilize each other's results.

Reactive Programming is a subset of Asynchronous Programming, which triggers code execution reactively, when data is presented, regardless of whether the function supposed to process it is already busy. Reactive Programming relies heavily on Message-Driven Architectures (where a message is typically an event or a command).

Both Asynchronous and Reactive applications are the ones that suffer greatly from blocking code - so using something like time.sleep() isn't a good fit for them. Let's take a look at some non-blocking code delay options.

Delaying Code with asyncio.sleep()

Asyncio is a Python library dedicated to writing concurrent code, and uses the async/await syntax, which might be familiar to developers who have used it in other languages.

Let's install the module via pip:

$ pip install asyncio

Once installed, we can import it into our script and rewrite our function:

import asyncio
async def main():
    print(datetime.datetime.now().time())
    await asyncio.sleep(5)
    print(datetime.datetime.now().time())

asyncio.run(main())

When working with asyncio, we mark functions that run asynchronously as async, and await the results of operations such as asyncio.sleep() that will be finished at some point in the future.

Similar to the previous example, this will print two times, 5 seconds apart:

17:23:33.708372
17:23:38.716501
Free eBook: Git Essentials

Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!

Though, this doesn't really illustrate the advantage of using asyncio.sleep(). Let's rewrite the example to run a few tasks in parallel, where this distinction is a lot more clear:

import asyncio
import datetime

async def intense_task(id):
    await asyncio.sleep(5)
    print(id, 'Running some labor-intensive task at ', datetime.datetime.now().time())

async def main():
    await asyncio.gather(
        asyncio.create_task(intense_task(1)),
        asyncio.create_task(intense_task(2)),
        asyncio.create_task(intense_task(3))
    )

asyncio.run(main())

Here, we've got an async function, which simulates a labor-intensive task that takes 5 seconds to finish. Then, using asyncio, we create multiple tasks. Each task can run asynchronously, though, only if we call them asynchronously. If we were to run them sequentially, they'd also execute sequentially.

To call them in parallel, we use the gather() function, which, well, gathers the tasks and executes them:

1 Running some labor-intensive task at  17:35:21.068469
2 Running some labor-intensive task at  17:35:21.068469
3 Running some labor-intensive task at  17:35:21.068469

These are all executed at the same time, and the waiting time for the three of them isn't 15 seconds - it's 5.

On the other hand, if we were to tweak this code to use time.sleep() instead:

import asyncio
import datetime
import time

async def intense_task(id):
    time.sleep(5)
    print(id, 'Running some labor-intensive task at ', datetime.datetime.now().time())

async def main():
    await asyncio.gather(
        asyncio.create_task(intense_task(1)),
        asyncio.create_task(intense_task(2)),
        asyncio.create_task(intense_task(3))
    )

asyncio.run(main())

We'd be waiting for 5 seconds between each print() statement:

1 Running some labor-intensive task at  17:39:00.766275
2 Running some labor-intensive task at  17:39:05.773471
3 Running some labor-intensive task at  17:39:10.784743

Delaying Code with Timer

The Timer class is a Thread, that can run and execute operations only after a certain time period has passed. This behavior is exactly what we're looking for, though, it's a bit of an overkill to use Threads to delay code if you're not already working with a multi-threaded system.

The Timer class needs to start(), and can be halted via cancel(). Its constructor accepts an integer, denoting the number of seconds to wait before executing the second parameter - a function.

Let's make a function and execute it via a Timer:

from threading import Timer
import datetime

def f():
    print("Code to be executed after a delay at:", datetime.datetime.now().time())

print("Code to be executed immediately at:", datetime.datetime.now().time())
timer = Timer(3, f)
timer.start()

This results in:

Code to be executed immediately at: 19:47:20.032525
Code to be executed after a delay at: 19:47:23.036206

The cancel() method comes in really handy if we have multiple functions running, and we'd like to cancel the execution of a function, based on the results of another, or on another condition.

Let's write a function f(), which calls on both f2() and f3(). f2() is called as-is - and returns a random integer between 1 and 10, simulating the time it took to run that function.

f3() is called through a Timer and if the result of f2() is greater than 5, f3() is canceled, whereas if f2() runs in the "expected" time of less than 5 - f3() runs after the timer ends:

from threading import Timer
import datetime
import random

def f():
    print("Executing f1 at", datetime.datetime.now().time())
    result = f2()
    timer = Timer(5, f3)
    timer.start()
    if (result > 5):
        print("Canceling f3 since f2 resulted in", result)
        timer.cancel()

def f2():
    print("Executing f2 at", datetime.datetime.now().time())
    return random.randint(1, 10)

def f3():
    print("Executing f3 at", datetime.datetime.now().time())

f()

Running this code multiple times would look something along the lines of:

Executing f1 at 20:29:10.709578
Executing f2 at 20:29:10.709578
Canceling f3 since f2 resulted in 9

Executing f1 at 20:29:14.178362
Executing f2 at 20:29:14.178362
Executing f3 at 20:29:19.182505

Delaying Code with Event

The Event class can be used to generate events. A single event can be "listened to" by multiple threads. The Event.wait() function blocks the thread it's on, unless the Event.isSet(). Once you set() an Event, all the threads that waited are awoken and the Event.wait() becomes non-blocking.

This can be used to synchronize threads - all of them pile up and wait() until a certain Event is set, after which, they can dictate their flow.

Let's create a waiter method and run it multiple times on different threads. Each waiter starts working at a certain time and checks if they're still on the hour every second, right before they take an order, which takes a second to fulfill. They'll be working until the Event is set - or rather, their working time is up.

Each waiter will have their own thread, while management resides in the main thread, and call when everyone can call home. Since they're feeling extra generous today, they'll cut the working time and let the waiters go home after 4 seconds of work:

import threading
import time
import datetime

def waiter(event, id):
    print(id, "Waiter started working at", datetime.datetime.now().time())
    event_flag = end_of_work.wait(1)
    while not end_of_work.isSet():
        print(id, "Waiter is taking order at", datetime.datetime.now().time())
        event.wait(1)
    if event_flag:
        print(id, "Waiter is going home at",  datetime.datetime.now().time())

end_of_work = threading.Event()

for id in range(1, 3):
    thread = threading.Thread(target=waiter, args=(end_of_work, id))
    thread.start()

end_of_work.wait(4)
end_of_work.set()
print("Some time passed, management was nice and cut the working hours short. It is now", datetime.datetime.now().time())

Running this code results in:

1 Waiter started working at 23:20:34.294844
2 Waiter started working at 23:20:34.295844
1 Waiter is taking order at 23:20:35.307072
2 Waiter is taking order at 23:20:35.307072
1 Waiter is taking order at 23:20:36.320314
2 Waiter is taking order at 23:20:36.320314
1 Waiter is taking order at 23:20:37.327528
2 Waiter is taking order at 23:20:37.327528
Some time passed, management was nice and cut the working hours short. It is now 23:20:38.310763

The end_of_work event was used here to sync up the two threads and control when they work and when not to, delaying the code execution by a set time between the checks.

Conclusion

In this guide, we've taken a look at several ways to delay code execution in Python - each applicable to a different context and requirement.

The regular time.sleep() method is pretty useful for most applications, though, it's not really optimal for long waiting times, isn't commonly used for simple scheduling and is blocking.

Using asyncio, we've got an asynchronous version of time.sleep() that we can await.

The Timer class delays code execution and can be canceled if need be.

The Event class generates events that multiple threads can listen to and respond accordingly, delaying code execution until a certain event is set.

Last Updated: March 2nd, 2023
Was this article helpful?

Improve your dev skills!

Get tutorials, guides, and dev jobs in your inbox.

No spam ever. Unsubscribe at any time. Read our Privacy Policy.

© 2013-2024 Stack Abuse. All rights reserved.

AboutDisclosurePrivacyTerms