Journey's End

Apr 20
2026

Datetime Display in Django

Suppose you have instruments in different timezones taking readings. You might have the following Django data model:

class Instrument(models.Model):
    timezone_utc_offset_hours = models.FloatField(...)
    ...

class Reading(models.Model):
    instrument = models.ForeignKey(Instrument, ...)
    timestamp = models.DateTimeField(auto_now_add=True)
    ...

Furthermore suppose settings.py contains the following:

...
USE_TZ = True
TIME_ZONE = "Europe/Paris"
...

It would be convenient to be able to display the timestamp for Readings in the timezone of the instrument. To do this, we need to convert the UTC timestamps stored in the database into local time of each instrument:

import datetime

class Instrument(models.Model):
    ...
    @property
    def timezone(self) -> datetime.timezone:
        utc_offset = datetime.timedelta(hours=self.timezone_utc_offset_hours)
        return datetime.timezone(utc_offset)

class Reading(models.Model):
    ...

    @property
    def local_timestamp(self) -> datetime.datetime:
        return self.timestamp.astimezone(self.instrument.timezone)

However if you were to display local_timestamp in a template, e.g. {{a_reading.local_timestamp | time}}, you will get the equivalent time in Paris, not the time where the instrument is located. This is because, when USE_TZ is set, TIME_ZONE in settings controls the default time zone used to display datetimes.

This time zone conversion can be made explicit by using a custom time format that includes the time zone offset: {{a_reading.local_timestamp | time:"H:i O"}}. This will produce something like

16:52 +0200

regardless of the value of timezone_utc_offset_hours due to automatic conversion to TIME_ZONE. In order to display the instrument-local time, we have to disable this automatic conversion using the localtime template tag:

{% load tz %}
...
{% localtime off %}
...
{{ a_reading.local_timestamp | time:"H:i O" }}
...
{% endlocaltime %}

With this change the display time is now in the instrument's timezone:

00:52 +1000

Template Tag Scoping

Note that due to the way templates work, {% localtime off %}...{% endlocaltime %} must be inside the same block as the time display. If specified at the template level, e.g.

{% extends "base.html" %}
{% load tz %}
{% localtime off %}
{% block page-content %}
...
    {{ a_reading.local_timestamp | time:"H:i O" }}
...
{% endblock %}
{% endlocaltime %}

It will have no effect inside the page-content block!