Search code examples
pythondatetimepytzpython-dateutil

Why do I get different results for pytz and dateutil for Samoa?


I was expecting that the following two would give the same result, but they didn't. Why is that the case?

Versions:

pytz==2018.5
python-dateutil==2.7.3

Example 1: pytz

import datetime
import pytz

tz = pytz.timezone('Pacific/Apia')
today_utc = datetime.datetime(2011, 12, 30, 9, 59,
                              tzinfo=datetime.timezone.utc)
today_tz = today_utc.astimezone(tz)
print(today_tz.isoformat())

prints 2011-12-29T23:59:00-10:00 (which is correct)

Example 2: dateutil

import datetime
import dateutil.tz

tz = dateutil.tz.gettz('Pacific/Apia')
today_utc = datetime.datetime(2011, 12, 30, 9, 59,
                              tzinfo=datetime.timezone.utc)
today_tz = today_utc.astimezone(tz)
print(today_tz.isoformat())

prints 2011-12-29T23:59:00+14:00 (which is wrong)


Solution

  • You have discovered a bug in dateutil, which I have now reported and fixed.

    The bug was caused by an issue with how the "wall time" of transitions were calculated in dateutil, which was making some assumptions that do not hold when a time zone's base offset changes during DST. Expanding your example a bit:

    from datetime import datetime, timedelta
    from dateutil import tz
    import pytz
    
    APIA = tz.gettz('Pacific/Apia')
    APIA_p = pytz.timezone('Pacific/Apia')
    dt0 = datetime.fromisoformat('2011-12-29T20:00-10:00')
    
    for i in range(5):
        dt = (dt0 + timedelta(hours=i))
        dt_d = dt.astimezone(APIA)
        dt_p = dt.astimezone(APIA_p)
        print(f'{dt_d.isoformat()}, {dt_p.isoformat()}')
    
    ## Result:
    # 2011-12-29T20:00:00-10:00, 2011-12-29T20:00:00-10:00
    # 2011-12-29T21:00:00-10:00, 2011-12-29T21:00:00-10:00
    # 2011-12-29T22:00:00-10:00, 2011-12-29T22:00:00-10:00
    # 2011-12-29T23:00:00+14:00, 2011-12-29T23:00:00-10:00
    # 2011-12-31T00:00:00+14:00, 2011-12-31T00:00:00+14:00
    

    You can see that dateutil always calculates the date and time correctly, but when isoformat calls utcoffset, the offset change happens 1 hour early. This is because astimezone calls tzinfo.fromutc under the hood, while isoformat calls utcoffset. dateutil stores the transition times in both UTC and local time, the UTC times are used in fromutc and the local times are used in utcoffset, dst and tzname. This bug involved over-compensating for DST when calculating the "wall time" of the transition during DST->DST transitions (which are exceedingly rare), which is why it didn't affect astimezone.

    Bottom line - you are using both pytz and dateutil correctly, and this error will be fixed in the next release.

    Note: This answer was edited after I found the cause of and fix for the bug.