Daylight savings time question

Carl Meyer carl at oddbird.net
Tue Mar 24 19:13:48 EDT 2015


Hi Dan,

On 03/24/2015 04:24 PM, Dan Stromberg wrote:
> Is there a way of "adding" 4 hours and getting a jump of 5 hours on
> March 8th, 2015 (due to Daylight Savings Time), without hardcoding
> when to spring forward and when to fall back?  I'd love it if there's
> some library that'll do this for me.
> 
> #!/usr/bin/python
> 
> import pytz
> import datetime
> 
> def main():
>     # On 2015-03-08, 2:00 AM to 2:59AM Pacific time does not exist -
> the clock jumps forward an hour.
>     weird_naive_datetime = datetime.datetime(2015, 3, 8, 1, 0,
> 0).replace(tzinfo=pytz.timezone('US/Pacific'))
>     weird_tz_aware_datetime =
> weird_naive_datetime.replace(tzinfo=pytz.timezone('US/Pacific'))
>     print(weird_tz_aware_datetime)
>     four_hours=datetime.timedelta(hours=4)
>     print('Four hours later is:')
>     print(weird_tz_aware_datetime + four_hours)
>     print('...but I want numerically 5 hours later, because of
> Daylight Savings Time')
> 
> main()

Much like the best advice for handling character encodings is "do all
your internal work in unicode, and decode/encode at your input/output
boundaries", the best advice for working with datetimes is "do all your
internal work in UTC; convert to UTC from the input timezone and
localize to the output timezone at your input/output boundaries." UTC
has no daylight savings time, so these issues disappear when you do your
calculations in UTC, and then the resulting dates are appropriately
handled by pytz when converting to local timezone at output.

So in this case, the following code gives the correct answer:

    naive_dt = datetime.datetime(2015, 3, 8, 1)
    tz = pytz.timezone('US/Pacific')
    local_dt = tz.localize(naive_dt)
    utc_dt = pytz.utc.normalize(local_dt)
    four_hours = datetime.timedelta(hours=4)
    new_utc_dt = utc_dt + four_hours
    new_local_dt = tz.normalize(new_utc_dt)

Someone may point out that you can actually just use pytz.normalize() to
solve this particular problem more directly, without the conversion to
UTC and back:

    naive_dt = datetime.datetime(2015, 3, 8, 1)
    tz = pytz.timezone('US/Pacific')
    local_dt = tz.localize(naive_dt)
    four_hours = datetime.timedelta(hours=4)
    new_local_dt = tz.normalize(utc_dt + four_hours)

(On the last line here, tz.normalize() is able to see that it's been
given a datetime which claims to be PST, but is after the spring
transition so should actually be PDT, and fixes it for you, correctly
bumping it by an hour in the process.)

While it's true that for this specific case the non-UTC method is more
direct, if you're writing any kind of sizable system that needs to
handle timezones correctly, you'll still be doing yourself a favor by
handling all datetimes in UTC internally.

Also, unless you really know what you're doing, you should generally use

    a_pytz_timezone_obj.localize(naive_dt)

to turn a naive datetime into a timezone-aware one, instead of

    naive_dt.replace(tzinfo=a_pytz_timezone_obj).

The latter just blindly uses the exact timezone object you give it,
without regard for when the datetime actually is (thus fails to respect
whether the timezone should be in DST or not, or any other historical
local-time transitions), whereas the `localize` method ensures that the
resulting aware datetime actually has the correct "version" of the
timezone applied to it, given when it is.

You can easily observe this difference, because the "default" version of
a timezone in pytz is the first one listed in the timezone database,
which for many timezones is LMT (local mean time), a historical timezone
offset abandoned in the late 1800s most places, which is often offset
from modern timezones by odd amounts like seven or eight minutes. For
example:

    >>> import pytz, datetime
    >>> dt = datetime.datetime(2015, 3, 8, 1)
    >>> tz = pytz.timezone('US/Pacific')
    >>> tz
    <DstTzInfo 'US/Pacific' LMT-1 day, 16:07:00 STD>
    >>> bad = dt.replace(tzinfo=tz)
    >>> bad
    datetime.datetime(2015, 3, 8, 1, 0, tzinfo=<DstTzInfo 'US/Pacific'
LMT-1 day, 16:07:00 STD>)
    >>> in_utc = pytz.utc.normalize(bad)
    >>> in_utc
    datetime.datetime(2015, 3, 8, 8, 53, tzinfo=<UTC>)

Note that the timezone assigned to the `bad` datetime is US/Pacific LMT,
which hasn't been in use since Nov 1883 [1] and which is 7 minutes
offset from modern timezones, resulting in a very surprising result when
you then convert that to UTC. In contrast, localize() does the right
thing, using PST instead of LMT because it knows that's the correct
offset for US/Pacific at 1am on March 8, 2015:

    >>> good = tz.localize(dt)
    >>> good
    datetime.datetime(2015, 3, 8, 1, 0, tzinfo=<DstTzInfo 'US/Pacific'
PST-1 day, 16:00:00 STD>)
    >>> in_utc = pytz.utc.normalize(good)
    >>> in_utc
    datetime.datetime(2015, 3, 8, 9, 0, tzinfo=<UTC>)


Carl

 [1] https://github.com/eggert/tz/blob/master/northamerica#L409

-------------- next part --------------
A non-text attachment was scrubbed...
Name: signature.asc
Type: application/pgp-signature
Size: 819 bytes
Desc: OpenPGP digital signature
URL: <http://mail.python.org/pipermail/python-list/attachments/20150324/38b1b073/attachment.sig>


More information about the Python-list mailing list