If I want to get a datetime instance that represents current local time and that is tagged with the current local timezone (i.e., an “aware” object), the recommended way to do this right now is as follows:
from datetime import datetime
datetime.now().astimezone()
Now, if this code was to be executed on a mobile device that is physically moving across time zones, it is possible for the system time zone to change during program execution. In such cases, we should expect to get the current local time in either the old or new timezone. However, because of two separate API calls (now()
and astimezone()
) that both rely on querying the local timezone, we can actually end up with nonsensical values that do not represent the current point of time in either time zone.
Here’s an example demonstrating the issue:
from datetime import datetime
import os, time
def settz(tz): # Should work on most Unix systems
os.environ['TZ'] = tz
time.tzset() # Applies change
print(f"Current time = {time.time()}")
# Let's run datetime.now() in ET
settz('US/Eastern')
dt = datetime.now()
print(f"now() time = {dt.timestamp()}")
# Then assume we drive into the CT zone just before
# calling astimezone()
settz('US/Central')
dtz = dt.astimezone()
print(f"astimezone() = {dtz.timestamp()}")
Running it on my system right now:
Current time = 1717617045.704923
now() time = 1717617045.705231
astimezone() = 1717620645.705231
Notice that the difference between the last two rows is 3600 seconds, or about one hour! It’s actually the wrong current time, regardless of which time zone the value dtz
it represents.
The core problem is that the expression datetime.now().astimezone()
requires at least two lookups* of the system local time zone: (1) the call to datetime.now()
produces a naive instance by converting the system-generated time_t
to a datetime instance using the system local time zone but without attaching a tzinfo object; (2) The astimezone()
call again looks up the system local time zone to attach a tzinfo object containing the local offset from UTC. If the local time zone has changed between these two steps, we get a result that is not correct in either time zone.
Workaround?: One workaround I can think of for this is to first get a tzinfo
object corresponding to the local time zone and then to call now()
with that tzinfo:
local_tzinfo = datetime.now().astimezone().tzinfo # During a physical transition, will return at least one of the timezones encountered
datetime.now(tz=local_tzinfo) # Does not rely on localtime() at all
However, this is not intuitive. In fact, the code is probably not correct if the physical change of time zones also happens during a DST transition, since local_tzinfo
could represent a timezone on the other side of DST as compared to the timezone followed locally now()
.
Ideal Solution: I think the datetime
library should have an API that produces the current local “aware” datetime with just one atomic method call. Perhaps something like datetime.now(tz=timezone.local)
. Also the API documentation should mention what to expect when the system transitions through time zones so that users understand the nuances when developing for mobile platforms. Thoughts?
* CPython actually makes several more calls to the localtime()
in order to compute the fold
attribute correctly, which results in many more potential outcomes other than what I’ve shown above depending on when exactly the system changes time zones.