[Python-Dev] Aware datetime from naive local time Was: Status on PEP-431 Timezones (original) (raw)

Nick Coghlan ncoghlan at gmail.com
Fri Apr 10 12:38:54 CEST 2015


On 9 Apr 2015 23:15, "Alexander Belopolsky" <alexander.belopolsky at gmail.com> wrote:

Sorry for a truncated message. Please scroll past the quoted portion. _On Thu, Apr 9, 2015 at 10:21 PM, Alexander Belopolsky <_ alexander.belopolsky at gmail.com> wrote:

On Thu, Apr 9, 2015 at 4:51 PM, Isaac Schwabacher <ischwabacher at wisc.edu> wrote: > > > Well, you are right, but at least we do have a localtime utility hidden in the email package: > > > > > > >>> from datetime import * > > > >>> from email.utils import localtime > > > >>> print(localtime(datetime.now())) > > > 2015-04-09 15:19:12.840000-04:00 > > > > > > You can read <http://bugs.python.org/issue9527> for the reasons it did not make into datetime. > > > > But that's restricted to the system time zone. Nothing good ever comes from the system time zone... > > Let's solve one problem at a time. ... PEP 431 proposes to import zoneinfo into the stdlib, ... I am changing the subject so that we can focus on one question without diverting to PEP-size issues that are better suited for python ideas. I would like to add a functionality to the datetime module that would solve a seemingly simple problem: given a naive datetime instance assumed to be in local time, construct the corresponding aware datetime object with tzinfo set to an appropriate fixed offset datetime.timezone instance. Python 3 has this functionality implemented in the email package since version 3.3, and it appears to work well even in the ambiguous hour >>> from email.utils import localtime >>> from datetime import datetime >>> localtime(datetime(2014,11,2,1,30)).strftime('%c %z %Z') 'Sun Nov 2 01:30:00 2014 -0400 EDT' >>> localtime(datetime(2014,11,2,1,30), isdst=0).strftime('%c %z %Z') 'Sun Nov 2 01:30:00 2014 -0500 EST' However, in a location with a more interesting history, you can get a situation that would look like this in the zoneinfo database: $ zdump -v -c 1992 Europe/Kiev ... Europe/Kiev Sat Mar 24 22:59:59 1990 UTC = Sun Mar 25 01:59:59 1990 MSK isdst=0 Europe/Kiev Sat Mar 24 23:00:00 1990 UTC = Sun Mar 25 03:00:00 1990 MSD isdst=1 Europe/Kiev Sat Jun 30 21:59:59 1990 UTC = Sun Jul 1 01:59:59 1990 MSD isdst=1 Europe/Kiev Sat Jun 30 22:00:00 1990 UTC = Sun Jul 1 01:00:00 1990 EEST isdst=1 Europe/Kiev Sat Sep 28 23:59:59 1991 UTC = Sun Sep 29 02:59:59 1991 EEST isdst=1 Europe/Kiev Sun Sep 29 00:00:00 1991 UTC = Sun Sep 29 02:00:00 1991 EET isdst=0 ... Look what happened on July 1, 1990. At 2 AM, the clocks in Ukraine were moved back one hour. So times like 01:30 AM happened twice there on that day. Let's see how Python handles this situation $ TZ=Europe/Kiev python3 >>> from email.utils import localtime >>> from datetime import datetime >>> localtime(datetime(1990,7,1,1,30)).strftime('%c %z %Z') 'Sun Jul 1 01:30:00 1990 +0400 MSD' So far so good, I've got the first of the two 01:30AM's. But what if I want the other 01:30AM? Well, >>> localtime(datetime(1990,7,1,1,30), isdst=0).strftime('%c %z %Z') 'Sun Jul 1 01:30:00 1990 +0300 EEST' gives me "the other 01:30AM", but it is counter-intuitive: I have to ask for the standard (winter) time to get the daylight savings (summer) time. The uncertainty about how to deal with the repeated hour was the reason why email.utils.localtime-like interface did not make it to the datetime module. The main objection to the isdst flag was that in most situations, determining whether DST is in effect is as hard as finding the UTC offset, so reducing the problem of finding the UTC offset to the one of finding the value for isdst does not solve much. I now realize that the problem is simply in the name for the flag. While we cannot often tell what isdst should be and in some situations the actual DST status does not differentiate between the two possible times, we can always say whether we want to get the first or the second time. In other words, instead of localtime(dt, isdst=-1), we may want localtime(dt, which=0) where "which" is used to resolve the ambiguity: "which=0" means return the first (in UTC order) of the two times and "which=1" means return the second. (In the non-ambiguous cases "which" is ignored.)

It actually took me a long time to understand that the "isdst" flag in this context related to the following chain of reasoning:

  1. Due to various reasons, local time offsets relative to UTC may change, thus repeating certain subsets of local time
  2. Repeated local times usually relate to winding clocks back an hour at the end of a DST period
  3. "isdst=True" thus refers to "before the local time change winds the clocks back", while "isdst=False" refers to after the clocks are wound back

As Alexander says, you can reduce the amount of assumed knowledge needed to understand the API by focusing on the ambiguity resolution directly without assuming that the reason for the ambiguity is "end of DST period".

From a pedagogical point of view, having a separate API that returned 0, 1, or 2 results for a local time lookup could thus help make it clear that local time to absolute time conversions are effectively a database lookup problem, and that timezone offset changes (whether historical or cyclical) mean that the mapping isn't 1:1 - some expressible local times never actually happen, while others happen more than once.

For the normal APIs, NonExistentTimeError would then correspond with the case where the record lookup API returned no results, while the suggested "which" index would handle the two results case without assuming the repeated local time was specifically due to the end of a DST period.

Regards, Nick. -------------- next part -------------- An HTML attachment was scrubbed... URL: <http://mail.python.org/pipermail/python-dev/attachments/20150410/e9f371ff/attachment.html>



More information about the Python-Dev mailing list