Timezones

My company services several cities in the continental United States. For those major metropolitan cities, we only have to concern ourselves with three US timezones: US/Pacific, US/Central, and US/Eastern. Even without non-hour-offset timezones I still experience headaches on a weekly basis. Many stem from original sins within the code, the legacy of unfulfilled future-proofing, failed prophecies.

In this article I'll describe a couple experiences and lessons learned. Note that my experiences are primarily within the context of Python and Django.

datetime(..., tzinfo=mytz)

First of all, shoutout to Paul Ganssle's excellent article pytz: The Fastest Footgun in the West. It came as quite the surprise that passing a pytz timezone to the datetime constructor is incorrect - violating the principle of least surprise. It's easy enough to remedy, but it's a terrible pitfall.

I had premonitions that something was off, when I saw the strange timezone offsets, with +/- a few minutes tacked onto the expected hours. I'm not completely caught up with the transition to ZoneInfo, but I believe that the situation moving forward is vastly improved, and a ZoneInfo timezone can safely be passed to the datetime constructor.

Datetimes and dates

When first starting out working with timezones, it's easy to naively try timestamp.date(). The issue of course is that timestamps are universal "points in time", while dates are not: today, April 13, 2024, is only 24 hours long from the perspective of a given timezone. Without the timezone, a date is ambiguous.

I cut my teeth on this at my internship, where I created a metrics tracking dashboard. The dashboard was designed to report the number of events in a given day. But there was feedback that the metrics were different between East and West coast users. Which makes sense, if the report is using the user's timezone for aggregating timestamps by date. An event at 1am in New York will fall on different dates for West and East coast users.

The solution, therefore, depends on the situation and the requirements. Perhaps timestamps should be converted to the originating timezone's date. Or perhaps the user's timezone. Rarely, though, should a date be extracted from a timestamp without timezone conversion.

Naive datetimes

This continues to plague me at work, original sins from developers long since moved on.

At the Django docs' suggestion, I added the following to the codebase, in order to transform naive datetime warnings into errors:

warnings.filterwarnings(
    "error",
    r"DateTimeField .* received a naive datetime",
    RuntimeWarning,
    r"django\.db\.models\.fields",
)

This exposed so many subtle timezone issues, that the team quickly gave up and commented out the code within our local repos.

One of the most nefarious issues is setting a DateTimeField to a date. When persisted, this converts to midnight on the date in the server default timezone. Note that in Django, the default timezone is separate from the active timezone. At work, the default timezone is US/Pacific. I expect it would be a Herculean effort to clean this up. In the meantime, there's a long paragraph of text warning developers "here be dragons."

self.resolved_at = timezone.localtime()

This is harmless, but a personal pet peeve. Per the Django docs:

When support for time zones is enabled, Django stores datetime information in UTC in the database, uses time-zone-aware datetime objects internally, and translates them to the end user’s time zone in templates and forms.

Django stores datetime information in UTC in the database. Consider this method on a Django model:

def resolve(self):
  self.resolved_at = timezone.localtime(timezone=self.timezone)
  self.save()

First, it looks up the timezone property on the object. Then, timezone.localtime() gets the current time, converting it to the specified timezone. When calling save(), the datetime is converted back to UTC when it is persisted as UTC in the database. So many unnecessary steps and translations!

Timestamps should be in UTC, and converted to a desired timezone at usage, just as Django does internally.

Recommended reading

social