How is time.sleep implemented in CPython?

I recently got curious as to how the time.sleep function from Python’s standard library was implemented by CPython - and as it turns out, it’s pretty easy to figure out.

All I had to do was strace a minimal example Python program that utlizies the function:

1strace python3 -c 'from time import sleep; sleep(0.5); print("Done!");' 2> out.txt

And sure enough, near the very end of strace’s output, we see the following system call:

1pselect6(0, NULL, NULL, NULL, {tv_sec=0, tv_nsec=500000000}, NULL) = 0 (Timeout)

If it weren’t for the very familiar names of the struct timespec members being listed I wouldn’t have any idea that this syscall was the one responsible for performing the sleep operation. As I am unfamiliar with the select and pselect6 syscalls - a couple of questions arose:

  • What do select and pselect6 have to do with sleeping?
  • Why use those instead of specialized syscalls - nanosleep and clock_nanosleep?

As always - the answers lie in the manpages. select and pselect6 are used to monitor groups of file descriptors for readiness of certain IO operations (reading, writing, etc..) You pass them some file descriptors, and the syscalls block until:

  • at least one of them becomes ready for the specified IO operation
  • the call is interrupted by a signal handler
  • the timeout specified by the timeout argument expires

Therefore, a call to pselect6 of the following form:

1pselect6(0, NULL, NULL, NULL, const struct timespec *, NULL);

is equivalent of blocking until the timeout expires and returning, i.e. “sleeping” for the duration of the timeout. That’s all well and good - but it feels “hacky” - the syscalls aren’t being used for their original purpose at all - why not use syscalls designed for sleeping in the first place?

Well, as is often the case - the same question has been asked before. The answer is not concrete nor exciting: portability. The CHANGE HISTORY sections of select and nanosleep’s respective POSIX specification pages seem to indicate that select was first specified in ‘Issue 4 Version 2’ of the standard, released sometime in 1994, and that nanosleep was standardized in Issue 5, released sometime in 1997 - all according to ‘Single UNIX Specification’ on WikiPedia. According to this answer on the Unix&Linux StackExchange, nanosleep also seems to have made its way into Linux in version 2.0.30, released in April 1997. So select did seem to have a ~3 year headstart on nanosleep, at least as far as standardization goes.

That’s all well and good - but it’s 2023. and nanosleep has been present in the Linux kernel for seemingly 26 years. Why not use it?

Well, I went digging into CPython’s source code to find out. And as it turns out, if we take a peek at the source code for the time.sleep function at cpython/Modules/timemodule.c, we see the following bit of code:

2        ret = clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &timeout_abs, NULL);
3        err = ret;
4#elif defined(HAVE_NANOSLEEP)
5        ret = nanosleep(&timeout_ts, NULL);
6        err = errno;
8        ret = select(0, (fd_set *)0, (fd_set *)0, (fd_set *)0, &timeout_tv);
9        err = errno;

Huh? So CPython does actually support clock_nanosleep. In fact it prefers it, and only falls back to select if the former isn’t supported. So why does my system’s Python interpreter call select? Well, as it turns out Arch Linux’s “bleeding edge” package repository is still stuck on Python3 version 3.10.10. And as the official time.sleep documentation says:

Changed in version 3.11: On Unix, the clock_nanosleep() and nanosleep() functions are now used if available. On Windows, a waitable timer is now used.

We can confirm this by switching to the 3.10 branch on CPython’s GitHub mirror and checking the time.sleep definition again:

1#ifndef MS_WINDOWS
2        if (_PyTime_AsTimeval(secs, &timeout, _PyTime_ROUND_CEILING) < 0)
3            return -1;
6        err = select(0, (fd_set *)0, (fd_set *)0, (fd_set *)0, &timeout);

On 3.10, no checks for the availability of clock_nanosleep or nanosleep are made, and of course select is called unconditionally. Python 3.11 was released on Oct. 24, 2022., so it still did take them a fair amount of time to add support for those newer system calls.