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

Akira Li 4kir4.1i at gmail.com
Wed Apr 15 22:46:21 CEST 2015


Alexander Belopolsky <alexander.belopolsky at gmail.com> writes:

> 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.
>

It looks incorrect. Here's the corresponding pytz code:

  from datetime import datetime
  import pytz

  tz = pytz.timezone('Europe/Kiev')
  print(tz.localize(datetime(1990, 7, 1, 1, 30), is_dst=False).strftime('%c %z %Z'))
  # -> Sun Jul  1 01:30:00 1990 +0300 EEST
  print(tz.localize(datetime(1990, 7, 1, 1, 30), is_dst=True).strftime('%c %z %Z'))
  # -> Sun Jul  1 01:30:00 1990 +0400 MSD
  
See also "Enhance support for end-of-DST-like ambiguous time" [1]

[1] https://bugs.launchpad.net/pytz/+bug/1378150

`email.utils.localtime()` is broken:

  from datetime import datetime
  from email.utils import localtime

  print(localtime(datetime(1990, 7, 1, 1, 30)).strftime('%c %z %Z'))
  # -> Sun Jul  1 01:30:00 1990 +0300 EEST
  print(localtime(datetime(1990, 7, 1, 1, 30), isdst=0).strftime('%c %z %Z'))
  # -> Sun Jul  1 01:30:00 1990 +0300 EEST
  print(localtime(datetime(1990, 7, 1, 1, 30), isdst=1).strftime('%c %z %Z'))
  # -> Sun Jul  1 01:30:00 1990 +0300 EEST
  print(localtime(datetime(1990, 7, 1, 1, 30), isdst=-1).strftime('%c %z %Z'))
  # -> Sun Jul  1 01:30:00 1990 +0300 EEST
  

Versions:

  $ ./python -V
  Python 3.5.0a3+
  $ dpkg -s tzdata | grep -i version
  Version: 2015b-0ubuntu0.14.04

> 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.

"repeated hour" (time jumps back) can be treated like a end-of-DST
transition, to resolve ambiguities [1].

> 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.)
>
> An alternative solution would be make  localtime(dt) return a list of 0, 1
> or 2 instances, but this will probably make a common usage (the case when
> the user does not care which time she gets) more cumbersome.



More information about the Python-Dev mailing list