Enhance time.sleep to accept a datetime.timedelta

Today I’m reviewing some code and I want to suggest that the author write

_MY_POLLING_INTERVAL = datetime.timedelta(milliseconds=400)

# (... and then much later on)
time.sleep(_MY_POLLING_INTERVAL)

but it seems like what is currently the closest possible is that the author write
time.sleep(_MY_POLLING_INTERVAL.total_seconds()). This was previously discussed somewhat in 2009 (in the discussion that led to the creation of the total_seconds method), but time.sleep wasn’t enhanced at that time to accept a datetime.timedelta. Would it make sense today to enhance time.sleep to accept its parameter as a datetime.timedelta?

9 Likes

Can you explain what’s wrong with writing that out?

I commonly use this method to pass seconds into a method that needs seconds and I have a timedelta.

4 Likes

What is wrong with this?

time.sleep(0.4)
1 Like

I like this. Using typed arguments to replace comments is usually a win. What you wrote is superior to:

_MY_POLLING_INTERVAL = 0.4  # Time delta in seconds

If this were accepted, I would even like to see a linter rule to push making the call with the timedelta over a float.

1 Like

Please no. This is one of the things that kills proposals: the pressure to create churn. There is nothing wrong with sleeping a number of seconds, you don’t have to deprecate that (even in linter rules) and force projects to silence the warning by making a completely unnecessary change to the code.

Stating this pushes me from +0 to -0.5.

13 Likes

You can just disable (or not enable) the linter rules you don’t like. Or don’t even use a linter if you hate churn.

Linter rules are not deprecation, and no one is “forced” to do anything.

The point of linter rules is to nudge programmers to write code that is easier to read. And sleeping with a time delta is easier to read than sleeping with a floating point number since the reader doesn’t have to wonder about the scale or type. (Is it second or milliseconds? Is it a floating point number or an integer?)

4 Likes

Your suggestion would imply that it would be a rule somewhere, though. And there will be drive-by PRs and issues that have no benefit to projects, just changing something to the “new way” of doing things.

That’s arguable, very arguable. Linter rules should only be used when there is a clear benefit, either because one way is distinctly better than another, or because project-wide consistency helps. Neither of those is true here. It’s seconds, and it doesn’t matter whether it’s a float or an int. Forcing people to import another module just to be able to satisfy a linter is NOT an improvement.

The original proposal may have merit, but tacking on a linter rule suggestion does not help it.

3 Likes

The time module is a step lower-level than datetime, it would not be appropriate for time to depend on datetime as it would need to in order to support passing a timedelta to sleep. At best you could reimplement time.sleep as something like

def sleep(s):
    return _real_sleep(getattr(s, 'total_seconds', lambda: s)())

but that too feels out of place for the time module.

I think you might actually get more traction with the suggestion of adding a sleep method to timedelta:

class timedelta:
    ...
    def sleep(self):
        return time.sleep(self.total_seconds())
    ...
21 Likes

The people who determine the enabled rules are project owners. You can do whatever you like in your own projects.

Well, anyway, it’s my opinion. If it is ever an issue on a linter, then you can make your argument there.

2 Likes

I like adding the .sleep() to timedelta in theory but wonder if that would lead to a desire for other functions like an equivalent to asyncio.sleep().

At the end of the day I’m not really + or - on the whole thing.

1 Like

Thanks for the engagement and contributions here! :slightly_smiling_face: (My apologies that I wasn’t able to respond as quickly as I would have liked. :slightly_frowning_face:)

Can you explain what’s wrong with writing […] out [`.total_seconds`]?

I wouldn’t call it “wrong”, but I would call it “less-nice-than” or “unergonomic”. The standard library affords a function to do something (well, in this case, nothing) for a duration. The standard library also affords a class that represents a duration. It makes perfects sense to want to pass an instance of the latter directly to the former. Looked at this way, the .total_seconds() remains not “wrong” but more of a “speed bump”, both in the writing and reading of code that has a timedelta and wants to use it with time.sleep.

What is wrong with [time.sleep(0.4)]?

I don’t want to have to find anything wrong with it, but if I really twist my mind around it, I could hand-wave in the direction of “what if some engineer works in Java for a few hours, where sleep takes milliseconds as a parameter, and then switches to Python for a task, and makes an off-by-a-multiplier-of-one-thousand error with zir argument to Python’s time.sleep?”. One remedy to this could be to use something like the argument comments that clang checks, so that the code always reads “this 0.4 is a measurement of seconds”. (As of the Python 3.13.7 that I have available to me, I do not seem able to pass time.sleep’s argument by the keyword “secs".)

… but I don’t find this possibility anywhere near strong enough to make the case for removing the existing use of scalar ints and floats; this proposal continues to make far more sense as a strictly-additive enhancement request leaving the present semantics also fully supported.

If this were accepted, I would even like to see a linter rule to
push making the call with the timedelta over a float.

This would make sense for the codebases in which I work, but I don’t see that it would work for all codebases everywhere, so I wouldn’t advocate for such a linter rule being on by default.

The time module is a step lower-level than datetime, it would not
be appropriate for time to depend on datetime as it would need to
in order to support passing a timedelta to sleep.

This is surprising for me to hear; as a long-time user of the standard library I don’t think I’ve ever come across any of its internal layers having these kinds of externally-visible consequences. This consideration could be kept internal to the standard library though, couldn’t it? This enhancement request could be implemented in such a way that the standard library would continue to show its “all at the same level of the abstraction hierarchy” that it does today, yes?

(Thanks again for the comments here! :slightly_smiling_face:)

3 Likes

There’s always going to be SOME sort of hierarchy, in the sense that nobody wants circular imports. (For example, re.MULTILINE is an enum, so it would be awkward for the enum module to make any use of regular expressions. It can be done, but needs to be managed carefully.)

The usual way to do this sort of thing is to define a protocol. And actually, time.sleep() already demonstrates this. You want to create your own numeric type that can behave like a float? You can do that:

# Let's pretend that this isn't hard-coded to a single value....
class WordNumber:
    def __float__(self):
        return 1.5
    def __repr__(self):
        return "One and a half"

>>> WordNumber()
One and a half
>>> time.sleep(WordNumber())
>>> 

The sleep function is quite happy to work with any class whatsoever, so long as it follows the protocol and reports itself as a float.

If this sort of thing is wanted - and personally I’m dubious of the value, but if - the way to do it would be to create a __seconds__ or __duration__ protocol, which returns the length of time that an object represents. This can then be used by time.sleep(), and anything else that requires a time duration (including timeouts eg socket, subprocess), and would be interpreted accordingly.

Thing is, though, Python’s standard library is already pretty consistent on that. I can’t think of anything where a timeout or delay is measured in anything other than seconds. (Watch me be proven wrong by some really obvious example, though.) We can’t control anything in other languages, and I’m not sure how much we gain by having a different way to do timeouts/sleeps.

So… maybe, just maybe, the right way forward is for timedelta to be floatable???

2 Likes

I find that to be a far better idea than anything presented in my previous post.

I think this is the opposite of the proposal. The proposal is aiming to make calls to sleep more legible by allowing callers to pass a structured type.

If you make sleep accept anything that supports float, then a reader of code involving sleep(some_timedelta) has to learn that timedelta can be converted to float, and she has to learn that the float it’s converted to happens to represent the exact same scale that sleep accepts.

On the other hand, if sleep explicitly accepts timedelta, then it’s obvious that it does the right thing, and the reader doesn’t have to wonder why.

In general, narrow interfaces are better than wide ones because the limits of the interface provide guardrails so that programmer missteps are caught early by static type checking or runtime errors. Making timedelta support float means that it can then accidentally be passed to all kinds of functions that accept such things and the programmer loses the useful warning—the guardrails. This can be convenient for small scripts. But for large programs, this is poor language design.

Incidentally, this also contradicts standard OO theory (the kind you find in textbooks), which says that interfaces should not contain methods that can be implemented using the rest of the public interface. Doing so creates monolithic classes, which are hard to learn and understand.

Since sleep can be implemented using timedelta’s public interface (by accessing total_seconds()), it should not be a method of timedelta. At least that’s what the theory says.

This is a really good point. I think the idealistic approach would be to invest some effort into repairing the hierarchy to support the proposal. For example by folding in the timedelta stuff into time. Or by reversing the hierarchy. Or by splitting time.

1 Like

Yes. If timedelta.__float__() returns the total seconds in the time delta, then you could use a timedelta object in a call to time.sleep(), which would interpret it as a float, and thus sleep for that many seconds.

The downside is, it makes a timedelta function as a float in all contexts, not just sleep() calls. And the only way to deal with that would be to create an entirely new protocol for getting the duration of an interval, which feels a bit overengineered.

It already accepts anything with a __float__ method. The implication of that method is “this can be treated as a float”, which is much narrower than “this can be converted to a float”. I think the rest of your post is built on a misunderstanding of that distinction, and given that misunderstanding, you wouldn’t be wrong.

I don’t think that’s necessary; a protocol resolves that issue. Whether that’s adding timedelta.__float__ or creating an entirely new .__total_seconds__() protocol (or even using .total_seconds() itself), it will avoid the need for time.sleep to be directly aware of what a timedelta object is - only that it’s an object that follows this protocol.

1 Like

Please, I just explained why I think this goes against the proposal:

That’s one downside of the protocol, yes. But it’s no the only downside.

A protocol that currently exists for a single class is a lot of engineering when you can simply support only passing the class itself.

Protocols are for defining “what an object can do” without requiring inheritance. Unlike base classes, they let you type-check existing classes you don’t control (including built-ins like int or third-party types) and objects that already satisfy a behavior (such as anything with a __call__ method). This makes it possible to express duck-typed interfaces explicitly, even when you can’t or shouldn’t add a common base class.

But we don’t have any such situation here. We’re not trying to “type-check existing classes that we don’t control”, and there is exactly one class whose behavior we’re trying to abstract: timedelta. Therefore, it is a lot of unnecessary complexity to add a special protocol for this. And not just in the Python codebase, but this is complexity that readers of code need to learn: to understand why sleep accepts timedelta, they would need to learn about this protocol and that sleep accepts objects that implement it. This is way too complicated for no benefit.

It is so much simpler to have sleep accept timedelta, and to simply fix the library hierarchy, which is an implementation detail.

sleep already accepts SupportsFloat. Your supposed advantage of a narrower interface (I don’t view it as an advantage) doesn’t apply here, nor is restructuring everything simpler than just implementing __float__

3 Likes

I don’t understand. It already supports floats, and everything with a __float__ method counts as a float. I demonstrated this above. What do you mean by “accept anything that supports float”?

My mistake, I didn’t realize that it already did the conversion. Still, I think that making timedelta convertible to float does add complexity to reading code since the reader has to verify and learn that when timedelta is converted to float, it has the same scale (seconds) as what sleep wants. On the other hand, if sleep were to accept timedelta explicitly, there is no such question.

1 Like

I think the status quo is not just okay but better than the alternatives proposed so far.

I’ll start with my position on the idea of special-casing timedeltas inside of time.sleep:
I consider this a bad design, even though it is easy to implement. Loose coupling is better than tight coupling, all things being equal, and this is a core strength of Python – duck typing and protocols are all about loose coupling. Layers of abstraction also matter, so I don’t consider this merely an implementation detail. Clear layering between “high level” and “low level” interfaces is a positive trait for a design.

However! I also think that giving timedelta a __float__ method would be a mistake. Being able to pass durations to math.ceil and other such functions makes little sense. Currently, passing a timedelta to a numeric function is a type error, detectable statically and at runtime. Turning type errors into logical errors is a significant negative, so we’d need a strong justification for it.

In particular, I feel I should address this response to @notatallshaw’s question:

I can see where this comes from, but I don’t agree that using total_seconds() is any less nice.

No extra import or other “heavy” mechanism is needed, just a method call. And given that code is written once and read many times, more explicit phrasing strikes me as good. Consider these two snippets:

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

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

Yes, I’ve intentionally chosen some odd naming in that example, but the ambiguity is precisely what I mean to emphasize.

5 Likes