Enhance time.sleep to accept a datetime.timedelta

I like your example. But don’t you think that this is even cleaner:

def parse_time(...) -> timedelta: ...

x = parse_time(minutes_past_midnight)
time.sleep(x) 

Now you don’t need to know that sleep needs seconds. The guarantees are not only visible to you from the static types, but they’re also checked by the type checker.

1 Like

Maybe you’re making a more subtle point than I understand, but I don’t see a difference between what you have written and my example? Other than the fact that you show a type signature for a function which I omitted. That omission was intentional, since it may be very distant from the call site.

It seems to me that if time.sleep accepts more types, then time.sleep(x), for unknown x, becomes more ambiguous.

On its own, time.sleep(foo.total_seconds()) suggests strongly that foo is a timedelta. That hint is especially useful when context clues could produce a misleading impression that foo is a numeric type with some other units (minutes, milliseconds, decades, jiffies, etc).

My opinion, at least for the moment, is unchanged.

I agree with what you’re saying. What I’m saying is that since you are the author of parse_time, and you know that parse_time returns a timedelta, then you do know that you are passing a timedelta to sleep, and you do know that sleep is doing the right thing (or else it would fail statically).

On the other hand, while I agree that in your preferred example, you also know that foo is a timedelta, and you know that you’re getting seconds from it, you still have to check that sleep accepts seconds.

So, essentially passing around timedelta objects allows you to forget about an arbitrary choice in sleep’s interface. It’s about being able to “turn your brain off” and focus on the details that actually matter.

Let’s not assume that the reader is also the author. Maybe parse_time was written 15 years ago and I inherited it last week.

We’re frequently working with only partial knowledge of the code which we are reading. We might not know the type of a variable – at least, not with certainty – and that seeing how it is used can help us understand what it is or is supposed to be.

1 Like

Does it? [edit: yes, it does, but only from Python 3.15, see below]

>>> class Z:
...     def __float__(self): return 1.234
...
>>> math.sin(Z())
0.9438182093746337
>>> time.sleep(float(Z()))
>>> time.sleep(Z())
Traceback (most recent call last):
  File "<python-input-7>", line 1, in <module>
    time.sleep(Z())
    ~~~~~~~~~~^^^
TypeError: 'Z' object cannot be interpreted as an integer

OTOH, sleep does convert to int implicitly:

>>> class Z:
...     def __float__(self): return 1.234
...     def __index__(self): return 42
...     def __int__(self): return -42
...
>>> start=time.time(); time.sleep(Z()); end=time.time(); print(end-start)
42.00052738189697

So it seems (at least in Python 3.13):

  • sleep implicitly converts to int (by __index__ method)
  • sleep needs actual float (or subclass) for sub-second precision

I couldn’t find that in the documentation.

Perhaps typeshed should type sleep as def sleep(seconds: float | SupportsIndex, /) -> None: ... instead of def sleep(seconds: float, /) -> None: ...?
[edit 2: typeshed issue 15313]

Edit: for above, Python 3.14 resembles 3.13. But in Python 3.15 (current a5), sleep does support float:

Python 3.15.0a5 (tags/v3.15.0a5:d51cc01, Jan 14 2026, 15:09:24) [MSC v.1944 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> class Z:
...     def __float__(self): return 1.234
...
>>> import time
>>> time.sleep(Z())
>>> exit
2 Likes

Oh interesting. I didn’t actually try it in all versions. Apologies for that.

Fair enough, you’re right! In that case, sure, using static types is not much of an improvement.

But in other cases, you’ll have a constant like

sleep_time = timedelta(seconds=12)

and then the type is obvious and it is an improvement to pass an object like that into sleep.

This really illustrates the idea that passing in a timedelta removes uncertainty that it’s doing what you expect!

True that it’s ambiguous, although honestly that only an evil type like below

>>> class Z:
...     def __float__(self): return 1.234
...     def __index__(self): return 4
...     def __int__(self): return 8
...

would expose the ambiguity (in Python 3.15):

>>> start=time.time(); time.sleep(Z()); end=time.time(); end-start  # __index__
4.000746488571167

sleep prefers __index__, and only fallbacks to __float__ if it must:

>>> class Y:
...     def __float__(self): return 2.34
...
>>> start=time.time(); time.sleep(Y()); end=time.time(); end-start  # __float__
2.3406224250793457

If we would add timedelta to the allowed parameter types, the above Z and Y would still be confusing. That ambiguity can only be removed by explicit conversions:

>>> start=time.time(); time.sleep(float(Z())); end=time.time(); end-start  # __float__
1.2346432209014893
>>> start=time.time(); time.sleep(int(float(Y()))); end=time.time(); end-start  # int of __float__
2.000532627105713

So while agreeing use of timedelta (either by totalseconds or by direct support in sleep) removes uncertainty, other cases remain uncertain.

1 Like

This was fixed by @storchaka in Inconsistency in datetime.utcfromtimestamp(Decimal) · Issue #67795 · python/cpython · GitHub

Python 2 already supported it apparently.

1 Like

Right, that’s why in my own projects, I would probably turn on a linting rule to push people not fall into those other cases :slight_smile:

This is the best and worst comment thus far.

This explains exactly why the status quo is the best solution:

A timedelta isn’t just a “duration”: it’s specfically the difference between two dateime or date instances.

and this is the worst response because naming a timedelta minutes_past_midnight, even just for rhetoric purposes, is evil.

4 Likes

There’s been a lot of discussion about timedelta being a numeric type in another thread recently.

It is a numeric type, it just also has a dimension (seconds).

Converting to float via __float__ would be fine in cases that deal in values of that dimension.(such as sleep). The only real problem here is the lack of protocol to enforce only converting when dealing in implicit seconds.

1 Like

I still think implementing __float__ on timedelta is the best option here. Anyone wanting dimensional analysis in general from the type system would need to propose that separately. There are many existing cases in the type system where that’s already not done, and shouldn’t prevent using the obvious solution here.

I don’t want to encourage you, but there is a use case for __duration__() which cannot be answered by __float__() and __seconds__(). There are many functions which take the timeout argument. But some of them require time in seconds, others in milliseconds or microseconds. Maybe even in nanoseconds or 1/100th seconds or 1/100 millions seconds. Because an underlying OS function uses such time units. Representing them as float can cause errors, because 0.001 > 1/1000, but 0.009 < 9/1000.

But what should hypothetical __duration__()return? It cannot be float. Integer nanoseconds? But what if we need to specify subnanosecond precision? If the custom timedelta-like class supports picosend precision, how should it round it to nanoseconds? We can get double rounding error. The safe bet is to return Decimal or Fraction with arbitrary precision, but this is heavy machinery.

And if we talk about relative duration, we should not forget about absolute timestamps. The problem is worse, because large absolute value close to 2**31 is a norm, and nanosecond precision adds other 30 bits.

3 Likes

Well, OBVIOUSLY, it should return an integer of Planck times.

3 Likes

Presumably, any API that required such precision wouldn’t use __float__ to begin with, and therefore implementing __float__ for timedelta to solve the current case shouldn’t be an issue, especially if this is documented for timedelta.

Or in the alternative, presumably users wouldn’t pass timedeltas to such functions.

I’m still unclear what’s so bad about sleep(interval.total_seconds()).

Implementing __float__ on timedelta means that there’s an obvious interpretation of float(interval). And I simply don’t think that’s true. We’re all very used to the idea that intervals are in seconds, but that’s just a convention. In the database world, Oracle’s intervals are expresed as fractional days, for instance. And for high-precision use, expressing intervals in nanoseconds (or milliseconds, on older systems) is common.

Having sleep accept datetime values is basically just a minor convenience, and IMO not worth the cost of mixing abstraction levels (the datetime module is explicitly a higher level abstraction than the time module, which expresses the operating system’s concept of time).

5 Likes

I personally don’t have an issue with using total_seconds explicitly. In my own code, even if this were implemented, I’d still prefer doing this than relying on __float__ as a matter of code convention with converting between value representations that have and don’t have a dimension.

That said, I don’t see an issue with a value that has a dimension being able to be converted to a scalar value absent the dimension implicitly in APIs that handle scalar values. It’s up to users to only pass this where it would make sense to do so. For timedelta, that would be places that expect seconds.

If people want rigorous static dimensional analysis, there are languages that do it better and ideas that could be borrowed from them, but I see that as a seperate concern.

Function takes timeout in milliseconds. You passed a timedelta object. No runtime error, wrong behavior.

2 Likes

The same happens for any number of other wrong argument types already. Not everything that implements __float__ is valid as seconds. If this is a serious argument, the change to sleep to accept SupportsFloat should be reverted.

1 Like