Why is datetime.utcnow deprecated?

Just made an account to say that I agree with everyone in Deprecating `utcnow` and `utcfromtimestamp` saying that this is a bad idea. I feel it’s a case of one person forcing their own coding preferences on everybody else. I’ve not seen anything to convince me that the problems presented by utcnow and utcfromtimestamp are anything more than rare issues. Forcing people to use timezones doesn’t make Python’s handling of timezones any better and for a language that’s supposed to be readable and beginner friendly it’s now getting far too complicated just to get the time. I still have to look up how to handle timezones each time I come to use it.

I’ve just been in the process of trying to make and old project timezone aware but after getting part way through I’m just giving up and going back, even ChatGPT told me not to bother with them.

If we must have timezones then there should only be one type, and the lack of timezone should infer UTC.

3 Likes

The lack of timezone does not imply UTC though, it implies the local time zone, which is why the current functions/methods were deprecated (in the docs at least).

By calling datetime.utcnow(), the object you get back does not represent the current datetime in UTC (unless the computer’s timezone is set to UTC) and that is why Paul refers to it as a footgun

No. On my system, utcnow() does return UTC:

>>> datetime.datetime.utcnow()
datetime.datetime(2025, 4, 2, 16, 54, 48, 324665)
>>> os.system('date')
wo 02 apr 2025 18:54:50 CEST

My timezone is set to CEST:

$ cat /etc/timezone
Europe/Madrid

This is a properly installed Linux system. I don’t know about Windows.

2 Likes

The problem is, that is NOT a timezone-aware object. It is a naive object that happens to correspond to the current time in UTC. Compare:

>>> datetime.datetime.now()
datetime.datetime(2025, 4, 3, 4, 17, 11, 338382)
>>> datetime.datetime.now().astimezone()
datetime.datetime(2025, 4, 3, 4, 17, 12, 106275, tzinfo=datetime.timezone(datetime.timedelta(seconds=39600), 'AEDT'))
>>> datetime.datetime.utcnow()
datetime.datetime(2025, 4, 2, 17, 17, 12, 922374)
>>> datetime.datetime.now(datetime.UTC)
datetime.datetime(2025, 4, 2, 17, 17, 13, 738301, tzinfo=datetime.timezone.utc)

Only the last one of these is actually a UTC time. The correct way to have actual UTC time is to have it with the tzinfo, which means now(datetime.UTC) and not utcnow().

5 Likes

And in many cases that’s what users want, as already discussed in length in Deprecating `utcnow` and `utcfromtimestamp`

Frankly Python standard library datetime timezones have design choices I would consider such bad footguns that I would never recommend people use them unless they have an intricate understanding of said design choices.

@moderators why was this topic split? By removing context of this discussion we’re wasting time and energy repeating things already discussed there.

5 Likes

I am in favour of not deprecating.

If this was so from the beginning, I think everyone would be happy with it.

But now as utc...() functions have been around forever, advantages for usage in appropriate cases can be seen. e.g.:

  1. Sometimes the whole system is UTC based and there is no need for explicit timezone
  2. Various ad-hoc plugins, where datetime objects are not used in persistent manner.

For such, utc...() variants are more performant and convenient. And the fact that these have been around for a long time, I would be in favour of keeping them.

The rationale that timezone-naive object should represent local time is I guess valid, but I am not sure if it needs to be enforced as I don’t see why it can not represent UTC when reasonable.

Maybe red box at the top of documentation with short warning and explanation would be better together with defining utcnow() being a shorthand for

datetime.now(UTC).replace(tzinfo=None)

and not correct way to define UTC objects in wider Python’s ecosystem.

P.S. This transition was a bit annoying to me. And even though I have already transitioned, I would still go back to using utc...() functions in certain places.

I agree with a docs warning instead of deprecation. Lots of cases in existing code rely on a naive datetime with assumptions (like it must be UTC). Seems rather opinionated to force folks to change to still accomplish the same thing.

3 Likes

I’m glad it was deprecated (although I’d prefer that it never gets removed). It taught me something I was doing wrong. The workaround has been around for sufficiently long that I can make the switch without wrapping it in if sys.version_info >= (3, x) . My only wish is that I could write utc as a string since I can never find datetime.timezone.utc.

4 Likes

These comments were split off to a new topic because they revived a thread from more than a year ago

The deprecation already happened in Python 3.12

1 Like

Right, but the comment could have just been left as is, because there wasn’t really anything new to add. Instead by splitting it off it’s made it look like a new question, so we’re repeating discussions and points already made.

So, to add something new to the discussion, I’ve had time to mull with the python datetime pitfalls blog since I last posted in that thread, and I’ve come to the conclusion that IMO:

  1. Python datetime should emit a warning when any timezoned non-UTC datetime arithmetic is done
  2. Users should be warned off using any timezoned datetime arithmetic until Python sufficiently warns people during runtime

Here are some examples why:

>>> # It's 10pm on the 25th March 2023 in Paris, in 8 hours it will be 7am because of DST

>>> now = datetime.datetime(2023, 3, 25, 22, tzinfo=zoneinfo.ZoneInfo("Europe/Paris"))
>>> now + datetime.timedelta(hours=8)
datetime.datetime(2023, 3, 26, 6, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/Paris'))

>>> now = pendulum.datetime(2023, 3, 25, 22, tz="Europe/Paris")
>>> now + datetime.timedelta(hours=8)
DateTime(2023, 3, 26, 7, 0, 0, tzinfo=Timezone('Europe/Paris'))

>>> now = whenever.ZonedDateTime(2023, 3, 25, 22, tz="Europe/Paris")
>>> now + datetime.timedelta(hours=8)
>>> now.add(hours=8)
ZonedDateTime(2023-03-26 07:00:00+02:00[Europe/Paris])

You run into the same problem reversing the situation, bear in mind that 8 hours is the correct answer, which is 28,800 seconds:

>>> now = datetime.datetime(2023, 3, 25, 22, tzinfo=paris)
>>> then = datetime.datetime(2023, 3, 26, 7, tzinfo=paris)
>>> then - now
datetime.timedelta(seconds=32400)
>>> (then - now).seconds
32400

>>> now = pendulum.datetime(2023, 3, 25, 22, tz="Europe/Paris")
>>> then = pendulum.datetime(2023, 3, 26, 7, tz="Europe/Paris")
>>> then - now
<Interval [2023-03-25 22:00:00+01:00 -> 2023-03-26 07:00:00+02:00]>
>>> (then - now).seconds
28800

>>> now = whenever.ZonedDateTime(2023, 3, 25, 22, tz="Europe/Paris")
>>> then = whenever.ZonedDateTime(2023, 3, 26, 7, tz="Europe/Paris")
>>> then - now
TimeDelta(08:00:00)
>>> (then - now).in_seconds()
28800.0

I therefore continue to advise what I advised in the previous thread, and am now having to repeat, if you currently use datetime.datetime.utcnow() you are best served by replacing it with the expression which produces the identical result datetime.datetime.now(datetime.UTC).replace(tzinfo=None). Trying to change logic and replace APIs at the same time is fraught with risks.

However, now I would advocate further, unless you can convince yourself that your code is definitely not susceptible to the design choices that lead to the wrong results above, then avoid using all non-utc timezoned arithmetic with from the datetime standard library, and if you can use a different library for timezoned arithmetic, especially if you are not heavily testing and validating your logic.

Standard library datetimes without timezones remain intuitive, and good to model many real world systems and data stores.

2 Likes

I fully agree with you. The current way is not perfect but is sometimes good enough.

I think those issues are debatable. The underlying question is: should a timedelta object be treated as a physical duration or a logical calendar interval?

Let’s take your example and change it a bit:

>>> now = datetime.datetime(2023, 3, 25, 22, tzinfo=zoneinfo.ZoneInfo("Europe/Paris"))
>>> now + datetime.timedelta(days=1)
datetime.datetime(2023, 3, 26, 22, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/Paris'))

Do you consider this result to be correct? Is one day the same thing as 24 hours or is it different?

whenever seems to be doing something weird in its add method:

>>> now = whenever.ZonedDateTime(2023, 3, 25, 22, tz="Europe/Paris")
>>> now.add(hours=24)
ZonedDateTime(2023-03-26 23:00:00+02:00[Europe/Paris])
>>> now.add(days=1)
ZonedDateTime(2023-03-26 22:00:00+02:00[Europe/Paris])

Which can quickly get more confusing:

>>> now.add(hours=24, days=-1)
ZonedDateTime(2023-03-25 22:00:00+01:00[Europe/Paris])
>>> now.add(hours=-24, days=1)
ZonedDateTime(2023-03-25 21:00:00+01:00[Europe/Paris])
>>> now.add(hours=-24, days=1)
ZonedDateTime(2023-03-25 21:00:00+01:00[Europe/Paris])
>>> now.add(hours=-24, days=1) == now.add(hours=24, days=-1)
False   # Hmm!

>>> now.add(hours=24, days=-1).add(hours=-24, days=1)
ZonedDateTime(2023-03-25 21:00:00+01:00[Europe/Paris])
>>> now == now.add(hours=24, days=-1).add(hours=-24, days=1)
False   # Really?

# But it's commutative, right?
>>> now.add(hours=-24).add(days=1)
ZonedDateTime(2023-03-25 22:00:00+01:00[Europe/Paris])
>>> now.add(days=1).add(hours=-24)
ZonedDateTime(2023-03-25 21:00:00+01:00[Europe/Paris])
>>> now.add(days=1).add(hours=-24) == now.add(hours=-24).add(days=1)
False   # It's not!

See, whenever’s add method actually does different things with its hours and days arguments: hours adds a physical duration, while days adds a calendar interval. Worse, if you combine them, you get a hybrid operation of adding a physical duration and a calendar interval, in unspecified order.

The conclusion is that, if you use a single datatype (timedelta) or a single method (add) to expose both semantics, you invariably end up with confusing or unexpected results. I believe that we made the right decision in Apache Arrow where we expose two different datatypes: a calendar Interval and a physical Duration.

3 Likes

Yeah, and you also put it in a different category. Some people might mute the “Python help” category. I’m cc’ing some of the original participants: @pganssle @malemburg @zware

2 Likes

After going through this a bit more I think that deprecating is better after all.


Also, to address wall/absolute arithmetics a bit, maybe timedelta could have extra argument absolute=False? So that:

from datetime import datetime, timedelta
from dateutil import tz
NYC = tz.gettz('America/New_York')
dt1 = datetime(2018, 3, 11, 1, tzinfo=NYC)
dt2 = datetime(2018, 3, 11, 1, tzinfo=tz.gettz('America/Los_Angeles'))
td = dt2 - dt1
print(td)               # 3:00:00 ABS
print(dt2 == dt1 + td)  # True
print(dt2 == dt1 + timedelta(hours=3, absolue=True))   # True

This is in direct response to “A timedelta can be generated from between-zone subtraction — in which case it represents an elapsed duration — but the resulting object is indistinguishable from an timedelta generated as the result of a “same zone” operation.” of a mentioned article.

That is intuitive to the linguistic description of those operations. When I am just before DST, if I said “in 24 hours” or “in 1 day” they have different meanings and I would not expect to arrive at the same result.

As you say, it’s debatable, because I would say you end up with more confusing results if you pick one regardless of your input, such as a result of 32400 seconds between two datetimes when it was actually 28800 seconds.

As I say, I think the standard library timezoned datetime arithmetic should emit a warning, telling users to do arithmetic operations in UTC and then convert back to the required timezone. In fact, I would prefer an exception, but that’s not going to happen at this point

I think this is a wider problem with Python’s deprecation policy.

Is datetime.utcnow entirely correct? Perhaps not.

Does datetime.utcnow have a high maintenance burden? Does not seem to. It’s literally a single line, calling a helper function shared with datetime.now.

Is it a very niche function that nobody uses? On GitHub, there are 532k Python files using datetime.utcnow, and 2.2M using datetime.now.

And yet, it is deprecated and scheduled for removal. How many unmaintained projects will break whenever it gets removed? How much developer time did this deprecation burn? How much code that was working correctly has to be changed? It would be better to not remove things that are just ugly, especially if they are low-maintenance and commonly used, so that upgrading to new Python versions is easier.

6 Likes

Is it a foot-gun? Yes.

2 Likes

With this level of usage I think letting linters like bugbear handle it is better than breaking running code.

1 Like

I found datetime.utcnow very confusing when I had to learn to work with timezones.
Like datetime.datetime.utcnow().astimezone(datetime.UTC) is double-corrected. The existence of double-correction means that it is ambigious what “correcting for time zone” means - whether that should set the clock forwards or back.

There are far bigger footguns than this one. By this argument, pickle should have been deprecated and removed years ago.

I recognise there are problems with utcnow, and I agree the docs should heavily discourage its use, but backwards compatibility outweighs the benefits of removing it in this case.

6 Likes