4

Converting a timezone naive date time to a specific timezone gives a completely incorrect result.

import dateutil as du
import pytz    
du.parser.parse('2017-05-31T15:00:00').replace(tzinfo=pytz.timezone('Europe/London')).isoformat()

returns a one minute not one hour offset vs UTC

'2017-05-31T15:00:00-00:01'

I've seen a few datetime peculiarities before but this one is breathtaking.

Stephen Rauch
  • 47,830
  • 31
  • 106
  • 135
Ymareth
  • 649
  • 4
  • 15
  • 2
    Not quite sure why that warranted an immediate down vote with no comment. – Ymareth Jul 21 '17 at 16:08
  • 3
    what's dateutil (3rd party library, not in standard lib like datetime)? what's the result after `parse`? – tayfun Jul 21 '17 at 16:11
  • 1
    Its a fairly well know [package](https://dateutil.readthedocs.io/en/stable/). Parse produces a standard datetime object. – Ymareth Jul 21 '17 at 16:15
  • 2
    Can you add the output of `parse` to the question? It will be helpful to see where the problem is. – tayfun Jul 21 '17 at 16:17
  • I wouldn't say `datetime.replace` is fundamentally broken. It's [well documented](http://pythonhosted.org/pytz/) that pytz does not exactly adhere to the `tzinfo` API specified in the Python documentation, and that using a pytz time zone object as the `datetime.tzinfo` parameter is unreliable. – John Y Jul 21 '17 at 22:32
  • 1
    Either use a tzinfo compliant time zone interface like `dateutil.tz` or use `pytz.timezone("Europe/London").localize(my_datetime)`. – Paul Jul 21 '17 at 23:30
  • This is a [very](https://stackoverflow.com/questions/35631578/python-datetime-pytz-issue), [very](https://stackoverflow.com/questions/2659908/python-datetime-not-including-dst-when-using-pytz-timezone) [common](https://stackoverflow.com/questions/24856643/unexpected-results-converting-timezones-in-python) [misunderstanding](https://stackoverflow.com/questions/6410971/python-datetime-object-show-wrong-timezone-offset). (Some would argue those are all duplicate questions, and there are probably plenty more.) – John Y Jul 21 '17 at 23:49

2 Answers2

3

The main problem here is that you are using a pytz time zone. pytz zones do not follow the tzinfo interface and cannot be simply attached to datetime objects (either through the constructor or through replace). If you would like to use pytz time zones, you should use pytz.timezone.localize with a naive datetime. If the datetime is already timezone-aware, you can use datetime.astimezone to convert it between zones.

from dateutil import parser
import pytz

LON = pytz.timezone('Europe/London')
dt = parser.parse('2017-05-31T15:00:00')
dt = LON.localize(dt) 

print(dt)   # 2017-05-31 15:00:00+01:00

This is because pytz's interface uses localize to attach a static time zone to a datetime. For the same reason, if you do arithmetic on the now-localized datetime object, it may give similar improper results and you'll have to use pytz.timezone.normalize to fix it. The reason this is done this way is that, historically, it has not been possible to handle ambiguous datetimes using a Pythonic tzinfo interface, which changed with PEP 495 in Python 3.6, making pytz's workaround less necessary.

If you would like to pass a tzinfo to a datetime using replace or the constructor, or you would prefer to use the pythonic interface, dateutil's time zone suite implements a PEP 495-compliant tzinfo interface. The equivalent using a dateutil zone is:

from dateutil import parser
from dateutil import tz

LON = tz.gettz('Europe/London')
dt = parser.parse('2017-05-31T15:00:00').replace(tzinfo=LON)

print(dt)   # 2017-05-31 15:00:00+01:00
Paul
  • 10,381
  • 13
  • 48
  • 86
1

I have often had bad luck using replace() with tzinfo objects. I have however found this construct to be reliable:

Code:

def naive_to_aware(ts, tz):
    return tz.localize(ts)

Update from Comments:

From the (pytz DOCS)

Unfortunately using the tzinfo argument of the standard datetime constructors ‘’does not work’’ with pytz for many timezones.

It is safe for timezones without daylight saving transitions though, such as UTC

So it is not just bad luck, it is problematic for pytz objects with timezones having DST.

Test Code:

import dateutil as du
import pytz

print(naive_to_aware(du.parser.parse('2017-05-31T15:00:00'),
                     pytz.timezone('Europe/London')).isoformat())

Results:

2017-05-31T15:00:00+01:00
Stephen Rauch
  • 47,830
  • 31
  • 106
  • 135
  • It's not "bad luck". The pytz documentation states, close to the beginning: *This library differs from the documented Python API for tzinfo implementations*. It goes on to state that one way to use pytz time zones is with the `astimezone` method, and that you should specifically avoid using it as the `tzinfo` argument if it has daylight saving transitions. It explicitly says UTC is safe for `tzinfo`. So, it seems you have basically discovered for yourself what the pytz documentation recommends. ;) – John Y Jul 21 '17 at 22:36
  • @JohnY, thank you so much for that info. An incompatible API would certainly explain why I just thought it was luck... Updated to reflect the *why*. – Stephen Rauch Jul 21 '17 at 22:51
  • @StephenRauch Your first bit of code does something completely different than `.replace(tzinfo=tz)`. `replace` just attaches the `tzinfo` without modifying the time. Your code assumes that the code is already in UTC. This will give the wrong answer if you have a naive `datetime` representing, e.g. Eastern Time and you simply want to attach a zone to it. – Paul Jul 22 '17 at 00:11
  • 1
    Note, I downvoted because this is wrong and misleading. I'm happy to retract if/when it's edited. – Paul Jul 22 '17 at 00:13