I’ve been wanting to roll out this post for quite some time but it kept evading me. In fact I had bulk of this post written out a long time back, just finishing it up was missing.
So, in this post I will be talking about how to deal with datetime
objects in Python
and how to interact with them when you are storing
it in Postgres
. The aim of this post is to avoid all error
prone methods of timezone conversion of datetime objects as well as
those of storing them.
Datetime objects are probably one of the most used in any application. One obvious thing you’d need when designing a model, is probably the date of creation field. It actually gets trickier to tackle them as your models mutate1 after a while, and as surprising as it may sound, it is also fairly easy to miscalculate your datetimes, especially when you are handling multiple timezones. Ignoring this will bite you back hard someday.
So what’s so complicated about a simple datetime?
First a little bit of background before we dive in. Their are two
kinds of datetime
objects. Timezone aware and Timezone
unaware or Naive objects. You can skip this part if you know the
difference between the two.
A timezone aware object is one which has a timezone information associated to it. That is, looking at such an instance, we will be able to find out the corresponding time in another time zone. Also, at any given moment in time, we can look this object up and identify which exact moment in time (seconds since epoch2) does it represent.
On the other hand, a timezone unaware or a naive object has no clue what moment in time it represents.
So if I am in India and say that the time is 10 AM, the person sitting next to me will know that it is 10 in the morning here in India. S/He can correctly identify it because they are also in India along with me. But if I say the same thing to a friend over the phone and ask them to second guess my location, s/he will surely be not able to answer my question. However, if I say that the current time is 10 AM, IST, my friend should be able to identify that I am somewhere in India, precisely from the timezone information I supplied. The analogy should be clear by now. I advise to read this section again if it isn’t.
What does Postgres want?
Postgres will store any timezone aware datetime in UTC but will display it in the time zone of the server, session or the user.
You can inspect the timezone
setting for postgres by logging
into the psql
shell and running the following command:
dhanush=# show timezone;
TimeZone
--------------
Asia/Kolkata
(1 row)
To set a custom timezone, use the following command:
dhanush=# set timezone='UTC';
SET
dhanush=# show timezone;
TimeZone
----------
UTC
(1 row)
Here’s the same set of records in both IST
and UTC
respectively.
IST
dhanush=# select created_at from users;
created_at
----------------------------------
2014-09-11 12:14:33.867216+05:30
2014-09-15 12:23:27.384904+05:30
2014-09-15 12:24:29.668802+05:30
2014-09-19 18:27:27.426808+05:30
2014-09-23 18:18:37.022816+05:30
2014-09-25 13:04:04.779181+05:30
2014-10-16 18:30:14.939262+05:30
(7 rows)
UTC
dhanush=# select created_at from users;
created_at
-------------------------------
2014-09-11 06:44:33.867216+00
2014-09-15 06:53:27.384904+00
2014-09-15 06:54:29.668802+00
2014-09-19 12:57:27.426808+00
2014-09-23 12:48:37.022816+00
2014-09-25 07:34:04.779181+00
2014-10-16 13:00:14.939262+00
(7 rows)
So it is clear that postgres shows you any timezone aware datetime in the timezone that is set.
At this point, I would like to make it clear that I did NOT research about timezone unaware datetime entries in postgres.
Okay, so what does Python want?
Python supports both naive
objects as well as timezone aware
objects. But, it is advised to never use timezone naive datetime
objects. The reason being, that a naive datetime object gives the end
user no information about the moment in time that it represents.
Datetime objects become more useful when they contain a timezone.
Hello pytz, bye bye errors!
pytz is an incredibly useful library
for handling timezones in python
. It does most of the work for you.
I have a naive datetime object. Please help me!
Take a look at the snippet below and the error that it produces:
In [13]: import pytz
In [14]: from datetime import datetime
In [15]: now = datetime.now()
In [16]: now
Out[16]: datetime.datetime(2014, 10, 16, 14, 17, 51, 720507)
In [17]: ist = pytz.timezone('Asia/Kolkata')
In [18]: ist.localize(now)
Out[18]: datetime.datetime(2014, 10, 16, 14, 17, 51, 720507, tzinfo=<DstTzInfo 'Asia/Kolkata' IST+5:30:00 STD>)
In [19]: now.astimezone(ist)
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-19-6bce1c9dbf37> in <module>()
----> 1 now.astimezone(ist)
ValueError: astimezone() cannot be applied to a naive datetime
In the last command you can see that astimezone
works on aware
objects where as localize
works on naive
objects. The error
message is self explanatory so I wouldn’t go any deeper into this.
So, why don’t you use datetime.datetime.replace instead?
One common error in converting naive
objects to aware
ones is the
use of datetime.datetime.replace
. Here’s a short example
demonstrating why it isn’t ideal.
In [38]: ist
Out[38]: <DstTzInfo 'Asia/Kolkata' LMT+5:53:00 STD>
In [39]: stupid_utc_now = datetime.utcnow()
In [40]: stupid_utc_now
Out[40]: datetime.datetime(2014, 10, 16, 14, 26, 57, 150167)
In [41]: stupid_utc_now.replace(tzinfo=ist).utcoffset()
Out[41]: datetime.timedelta(0, 21180)
If you look carefully, the information about ist
is LMT+5:53:00
STD
which you might think is incorrect as the offset of IST
from
UTC
is +5:30
and not 5:53
. Bug? Not really. Initially IST
was
declared to be +5:53
ahead of UTC
which was corrected later on to
be +5:30
. But replace
doesn’t really know about this and thus you
can see the offset as 21180
seconds instead of 19800
seconds.
Now take a look at the following snippet:
In [42]: intelligent_utc_now = datetime.utcnow()
In [43]: ist.localize(intelligent_utc_now).utcoffset()
Out[43]: datetime.timedelta(0, 19800)
In [44]: old_days = datetime(1700, 6, 18)
In [45]: old_days
Out[45]: datetime.datetime(1700, 6, 18, 0, 0)
In [46]: ist.localize(old_days)
Out[46]: datetime.datetime(1700, 6, 18, 0, 0, tzinfo=<DstTzInfo 'Asia/Kolkata' LMT+5:53:00 STD>)
Now carefully note the two offsets of intelligent_utc_now
. You can
see the correct offset this time. Whereas, for old_days
, when
localized to IST, the offset is 5:53
. That’s the wonder of pytz
.
Depending on the time, it returns the offset accordingly. +5:53
for
a really old date and +5:30
for a date after the time that the
offset for IST was corrected.
Finally, if you want to work with the current time as a timezone aware object, which once again I highly advise you to use instead of a naive timezone object, you should do the following:
In [47]: datetime.now(pytz.timezone('Asia/Kolkata'))
Out[47]: datetime.datetime(2014, 10, 20, 13, 25, 14, 121485, tzinfo=<DstTzInfo 'Asia/Kolkata' IST+5:30:00 STD>)
And, if you need the current time in UTC,
In [48]: datetime.now(pytz.utc)
Out[48]: datetime.datetime(2014, 10, 20, 7, 55, 17, 817783, tzinfo=<UTC>)
or,
In [49]: pytz.utc.localize(datetime.utcnow())
Out[49]: datetime.datetime(2014, 10, 20, 7, 55, 23, 25311, tzinfo=<UTC>)
But in my opinion the first approach looks way more clean. For fans of
datetime.datetime.replace
, you can still use it for creating a
datetime
object in UTC without any problems.
TL;DR
Dos
- Always use
aware
datetime objects. - When dealing with
naive
objects, uselocalize
to add timezone - When you are converting timezones use
astimezone
.
Donts
- Create
naive
datetime objects. - Use
datetime.datetime.replace
for adding or manipulating timezone information on adatetime
object. Except when you use it to instantiate a datetime object in UTC.
Footnotes
[1] Read this post here if you want to know why I say your models mutate instead of change.