Dates and databases with Python
It's a bit tricky to deal with dates, timezones and daylight savings when you need to store dates in e.g. a database for later reading.
To me, it's a bit perplexing that all tools required to deal with this doesn't come with the Python standard library (meaning; batteries are not included). Instead we need to use three different modules: datetime, pytz and tzlocal where the two latter ones are not part of the standard library and must be installed separately via e.g. pip
.
Here follows some personal notes on how to store and read back dates with reliability and control of the timezones and daylight savings.
When reading the below code it helps to think that the tz
abbreviation means "timezone".
Getting the current local time
You can use now
to get the local time right now:
However, this does not tell us whether daylight savings was in effect or not. And we don't know from which timezone this date was taken if we were just given these numbers. This becomes a problem when storing the date in e.g. a database and later want to show this date in a user's local timezone.
Storing dates as UTC-aware
When storing dates in e.g. a database, store them aware of UTC (or "UTC-aware"):
>>> import datetime
>>> import pytz
>>> now = datetime.datetime.now(pytz.utc) # store this in db
>>> print(now)
2017-11-30 08:01:19.676817+00:00
Please note, the actual local time for me (who is UTC+1) is 09:01:19
and not 08:01:19
. But instead of storing my local UTC+1 datetime, we store a datetime which is just aware of UTC, thanks to pytz.utc
.
Reading UTC-aware dates back and showing them accurately
Later, when reading the dates back from e.g. a database, apply the user's local timezone and any daylight savings (in my case UTC+1 right now since I'm in Sweden).
>>> import datetime
>>> import pytz
>>> timezone = 'Europe/Stockholm'
>>> local_tz = pytz.timezone(timezone)
>>> local_dt = now.replace(tzinfo=pytz.utc).astimezone(local_tz)
>>> now_local = local_tz.normalize(local_dt) # show this to the user
>>> print(now_local)
2017-11-30 09:01:19.676817+01:00
Now I get the time I was expecting, my local time 09:01:19
, again thanks to pytz
and the tz database which it is using.
For a list of all timezones supported by pytz
, see here.
Avoid hardcoding the local timezone
In the previous code block, I'm hardcoding the timezone
variable. You may want to read the client system's timezone and just use that. This can be achieved using the tzlocal
module.
>>> import datetime
>>> import pytz
>>> import tzlocal
>>> local_tz = tzlocal.get_localzone()
>>> local_dt = now.replace(tzinfo=pytz.utc).astimezone(local_tz)
>>> now_local = local_tz.normalize(local_dt) # show this to the user
>>> print(now_local)
2017-11-30 09:01:19.676817+01:00
Warning: datetime's timedelta does not consider DST
This was added on 2022-11-06.
When using datetime.timedelta
to add or subtract time, it does not consider daylight savings. I created a gist here to illustrate the problem.
Closing comments
Having all this code finally assembled and condensed in a blog post like this is nice and neat, but why is this so hard to do currently, and why does this require three separate modules of which two are not included in the standard library?
I guess the answer is I should shut up because I'm already spoiled with Python? ;)
I would like to include a link to this fantastic article, written by Zach Holman. This touches on the history and complexity of managing timezones, which is not only educational but also quite funny reading. For those interested, here's also the Hacker News thread which followed after this article was published.