Pythonity Blog Pythonity Blog

My Brief Encounter with Daylight Saving Time

I never really cared about daylight saving time. Sure, I knew it existed, occasionally saw some articles making a case that it's not helpful in any way, but as long as my computer and phone knew when to switch - I was fine with it. One hour forward here, one hour back there, whatever. But I never really considered its practical implications, such as date management in software - like in this particular case.

A couple of weeks ago, a colleague of mine pointed out that when he copied events on our company instance of Schedoodle (which is our work schedule web app, built on Django) from week 21-27.03 onto the next one, some of them were off by one hour. I quickly found out that 27.03 is the magical day when daylight saving time change occurs, and 01:59:59 jumps to 03:00:00. It was obvious that this was the cause of the problem, so I had a starting point.

First, I looked at the code responsible for copying events from the previous week:

one_week = timedelta(weeks=1)

for prev_event in prev_week_events:
    start_datetime = prev_event.start_datetime + one_week
    end_datetime = prev_event.end_datetime + one_week


Nothing fancy - it just adds timedelta(weeks=1) to start and end datetimes. Hmm. Let's peek into the database - DateTimeField is smartly converted to UTC, and copying behaved as I thought it should - 2016-03-21 10:00:00 plus one week is 2016-03-28 10:00:00. Correct, isn't it? If only... Even though the timezone doesn't change, the hour offset for those dates does - 21.03.2016 is CET UTC+1h and 28.03.2016 is CEST UTC+2h. So when the datetimes are localized by Django, they get converted to 2016-03-21 11:00:00 and 2016-03-28 12:00:00 respectively. So I (kinda) know what's wrong but I still don't know which piece of code should be changed. When in doubt, Google it.

Both 'python daylight savings time' and 'django daylight savings time' point to a lot of similar Stack Overflow questions and blog posts but I would like to highlight those that helped me the most. First - and I should probably start with it both when the issue occurred and in this post - is Django's Docs about time zones. It's quite long, but mostly because the subject itself isn't trivial. It also points to pytz which is recommended by Django when more than basic timezone support is required.

But the key resource for me was Time Zones in Pytz & Django by Tommi Kaikkonen. I can't recommend it enough. It's quite lengthy but that's because it really exhausts the topic with multiple diagrams and code samples. It covers DST itself and then goes into explaining why it's such a buggy idea ending with examples of Python code with pytz and then how that all falls into Django itself. Also helpful - Troubles with timezones by Tikitu de Jager.

The essential part (for me) to understand from those links in this case was the difference between naive and aware datetimes and how that affects time addition operations with regards to DST and timezones. Both pytz and Django help a lot and have all the required pieces (have you seen django.utils.timezone?) - you just need to be, ekhm, aware of what you're working with and what you want to accomplish (I know, some shocking, eye-opening advice).

With all that newly acquired knowledge, I went back and reviewed all the code responsible for datetime manipulation. It wasn't bad (I was adding tzinfo=timezone.get_current_timezone to datetime on creation instead of using timezone.make_aware()), but it definitely could be made better - and now it is. As to the problem itself - understanding it was the main part. Most importantly, always operate on UTC datetimes and work it up from there. With that in mind, all it took were just a few simple changes:

one_week = timedelta(weeks=1)

for prev_event in prev_week_events:
    # Yay daylight savings time!
    tz = timezone.get_current_timezone()
    start_datetime = tz.localize(
        timezone.make_naive(prev_event.start_datetime) + one_week
    end_datetime = tz.localize(
        timezone.make_naive(prev_event.end_datetime) + one_week


Well, at least now I do have an opinion about daylight saving time. I don't like it.