tl;dr
To change from one timezone aware datetime to another, turn it into a naive datetime and then use pytz
's localize()
method to convert it back to the timezone you want it to be.
Introduction
Suppose you have a Django form where you allow people to enter a date, e.g. 2015-06-04 13:00
. You have to save it timezone aware, because you have settings.USE_TZ
on and it's just many times to store things in timezone aware dates.
By default, if you have settings.USE_TZ
and no timezone information is in the string that the django.form.fields.DateTimeField
parses, it will use settings.TIME_ZONE
and that timezone might be different from what it really should be. For example, in my case, I have an app where you can upload a CSV file full of information about events. These events belong to a venue which I have in the database. Every venue has a timezone, e.g. Europe/Berlin
or US/Pacific
. So if someone uploads a CSV file for the Berlin location 2015-06-04 13:00
means 13:00 o'clock in Berlin. I don't care where the server is hosted and what its settings.TIME_ZONE
is. I need to make that input timezone aware specifically for Berlin/Europe
.
Examples
Suppose you have settings.TIME_ZONE == 'US/Pacific'
and you let the django.form.fields.DateTimeField
do its magic you get something you don't want:
>>> from django.conf import settings
>>> settings.TIME_ZONE
'US/Pacific'
>>> assert settings.USE_TZ
>>> from django.forms.fields import DateTimeField
>>> DateTimeField().clean('2015-06-04 13:00')
datetime.datetime(2015, 6, 4, 13, 0, tzinfo=<DstTzInfo 'US/Pacific' PDT-1 day, 17:00:00 DST>)
See! That's wrong. Sort of. Not Django's fault. What I need to do is to convert that datetime object into one that is timezone aware on the Europe/Berlin
timezone.
In old versions of pytz
, specifically <=2014.2
you could do this:
>>> import pytz
>>> pytz.VERSION
'2014.2'
>>> from django.forms.fields import DateTimeField
>>> date = DateTimeField().clean('2015-06-04 13:00')
>>> date
datetime.datetime(2015, 6, 4, 13, 0, tzinfo=<DstTzInfo 'US/Pacific' PDT-1 day, 17:00:00 DST>)
>>> date.replace(tzinfo=tz)
datetime.datetime(2015, 6, 4, 13, 0, tzinfo=<DstTzInfo 'Europe/Berlin' CET+1:00:00 STD>)
But in modern versions of pytz
you can't do that because if you don't use the pytz.timezone
instance to localize it will use the default version which might be one of those crazy "Local Mean Time" which they used a 100 years ago. E.g.
>>> import pytz
>>> pytz.VERSION
'2015.7'
>>> from django.forms.fields import DateTimeField
>>> date = DateTimeField().clean('2015-06-04 13:00')
>>> tz = pytz.timezone('Europe/Berlin')
>>> date.replace(tzinfo=tz)
datetime.datetime(2015, 6, 4, 13, 0, tzinfo=<DstTzInfo 'Europe/Berlin' LMT+0:53:00 STD>)
See, it's that crazy LMT+0:53:00
that's oft talked of on Stackoverflow!
Here's the trick
The trick is to use pytz.timezone(MY TIME ZONE NAME).localize(MY NAIVE DATETIME OBJECT)
. When you use the .localize()
method pytz
can use the date to make sure it uses the right conversion for that named timezone.
And in the case of our overly smart django.form.fields.DateTimeField
it means we need to convert it back into a naive datetime object and then localize it.
>>> import pytz
>>> pytz.VERSION
'2015.7'
>>> from django.forms.fields import DateTimeField
>>> date = DateTimeField().clean('2015-06-04 13:00')
>>> date = date.replace(tzinfo=None)
>>> date
datetime.datetime(2015, 6, 4, 13, 0)
>>> tz = pytz.timezone('Europe/Berlin')
>>> tz.localize(date)
datetime.datetime(2015, 6, 4, 13, 0, tzinfo=<DstTzInfo 'Europe/Berlin' CEST+2:00:00 DST>)
That was much harder than it needed to be. Timezones are hard. Especially when you have the human element of people typing in things and just, rightfully, expect the system to figure it out and get it right.
I hope this helps the next schmuck who has/had to set aside an hour to figure this out.