Home > database >  Two ways to create timezone aware datetime objects (Django). Seven minutes difference?
Two ways to create timezone aware datetime objects (Django). Seven minutes difference?

Time:04-11

Up to now I thought both ways to create a timezone aware datetime are equal.

But they are not:

import datetime

from django.utils.timezone import make_aware, get_current_timezone

make_aware(datetime.datetime(1999, 1, 1, 0, 0, 0), get_current_timezone())

datetime.datetime(1999, 1, 1, 0, 0, 0, tzinfo=get_current_timezone())
datetime.datetime(1999, 1, 1, 0, 0, tzinfo=<DstTzInfo 'Europe/Berlin' CET 1:00:00 STD>)

datetime.datetime(1999, 1, 1, 0, 0, tzinfo=<DstTzInfo 'Europe/Berlin' LMT 0:53:00 STD>)

In the Django Admin GUI second way creates this (German date format dd.mm.YYYY):

01.01.1999 00:07:00

Why are there 7 minutes difference if I use this:

datetime.datetime(1999, 1, 1, 0, 0, 0, tzinfo=get_current_timezone())

CodePudding user response:

This happens on Django 3.2 and lower, which rely on the pytz library. In Django 4 (unless you enable to setting to use the deprecated library), the output of the two examples you give is identical.

In Django 3.2 and below, the variance arises because the localised time is built in two different ways. When using make_aware, it is done by calling the localize() method on the pytz timezone instance. In the second version, it's done by passing a tzinfo object directly to the datetime constructor.

The difference between the two is well illustrated in this blog post:

The biggest mistake people make with pytz is simply attaching its time zones to the constructor, since that is the standard way to add a time zone to a datetime in Python. If you try and do that, the best case scenario is that you'll get something obviously absurd:

import pytz
from datetime import datetime

NYC = pytz.timezone('America/New_York')
dt = datetime(2018, 2, 14, 12, tzinfo=NYC)
print(dt)
# 2018-02-14 12:00:00-04:56

Why is the time offset -04:56 and not -05:00? Because that was the local solar mean time in New York before standardized time zones were adopted, and is thus the first entry in the America/New_York time zone. Why did pytz return that? Because unlike the standard library's model of lazily-computed time zone information, pytz takes an eager calculation approach.

Whenever you construct an aware datetime from a naive one, you need to call the localize function on it:

dt = NYC.localize(datetime(2018, 2, 14, 12))
print(dt)
# 2018-02-14 12:00:00-05:00

Exactly the same thing is happening with your Europe/Berlin example. pytz is eagerly fetching the first entry in its database, which is a pre-1983 solar time, which was 53 minutes and 28 seconds ahead of Greenwich Mean Time (GMT). This is obviously inappropriate given the date - but the tzinfo isn't aware of the date you are using unless you pass it to localize().

This is the difference between your two approaches. Using make_aware correctly calls localize() on the object. Assigning the tzinfo directly to the datetime object, however, doesn't, and results in pytz using the (wrong) time zone information because it was simply the first entry for that zone in its database.

The pytz documentation obliquely refers to this as well:

This library only supports two ways of building a localized time. The first is to use the localize() method provided by the pytz library. This is used to localize a naive datetime (datetime with no timezone information)... The second way of building a localized time is by converting an existing localized time using the standard astimezone() method... Unfortunately using the tzinfo argument of the standard datetime constructors ‘’does not work’’ with pytz for many timezones.

It is actually because of these and several other bugs in the pytz implementation that Django dropped it in favour of Python's built-in zoneinfo module.

More from that blog post:

At the time of its creation, pytz was cleverly designed to optimize for performance and correctness, but with the changes introduced by PEP 495 and the performance improvements to dateutil, the reasons to use it are dwindling. ... The biggest reason to use dateutil over pytz is the fact that dateutil uses the standard interface and pytz doesn't, and as a result it is very easy to use pytz incorrectly.

Passing a pytz tzinfo object directly to a datetime constructor is incorrect. You must call localize() on the tzinfo class, passing it the date. The correct way to initialise the datetime in your second example is:

> berlin = get_current_timezone()
> berlin.localize(datetime.datetime(1999, 1, 1, 0, 0, 0))
datetime.datetime(1999, 1, 1, 0, 0, tzinfo=<DstTzInfo 'Europe/Berlin' CET 1:00:00 STD>)

... which matches what make_aware produces.

  • Related