User Timezones in Django
2025-07-28
When you create a local website, the local time usually matches your country’s timezone, and all visitors see times in that timezone. That’s not a big issue if your country has only one timezone and your audience is local.
But when building a social platform like pybazaar.com, users are international and need to see times in their timezones. In this article, I’ll show you how to handle that in Django.
Time Zone Database
Since version 4.0, Django has used the zoneinfo
library for managing timezones, and it used pytz
up to version 3.2. Both rely on the IANA Time Zone Database (tzdata
). IANA is the same organization that manages the DNS root zone, IP addresses, and other global internet resources.
Install tzdata
in your virtual environment as usual:
(venv)$ pip install --upgrade tzdata
Timezone Changes
Timezone information changes several times a year due to:
- Daylight Saving Time (DST) adjustments
- Political and border changes
- Shifts in standard time offset
Daylight Saving Time (DST) was first introduced in 1914 in Canada and later standardized in the U.S. in 1966. When dealing with historic dates before 1966—or future dates with uncertain timezone rules—precise time calculations can be unreliable.
# Before U.S. DST standardization:
old_date = datetime(1960, 6, 15, 12, 0)
# DST rules may change in the future:
future_date = datetime(2030, 6, 15, 12, 0)
Some timezone changes are driven by politics:
- Country splits or mergers — new countries may adopt different timezones.
- Regional preferences — states or provinces may change timezone alignment.
- Symbolic actions — e.g., North Korea introduced "Pyongyang Time" in 2015 by shifting 30 minutes back to symbolically break from Japan’s colonial legacy.
And countries sometimes adjust their UTC offsets:
- Russia — changed its timezone policy multiple times
- Venezuela — changed from UTC-4 to UTC-4:30 in 2007, then back in 2016
- Samoa — jumped from UTC-11 to UTC+13 in 2011, skipping Dec 30 entirely
Best Practices for Django
- Use named timezones, not fixed UTC offsets.
- Update
tzdata
monthly or quarterly. - Test with historic and future dates.
- Handle conversion errors gracefully, falling back to UTC.
- Store all times in UTC internally.
- Convert to user’s timezone only in the UI.
- Include
tzdata
updates in deployment (Docker, Ansible, etc.).
Timezone Management for a Social Platform
For platforms with global users:
- Store all datetimes in UTC.
- Store each user's preferred timezone.
- Convert times on input/output according to the user’s timezone.
1. Enable Timezone Support in Django Settings
Set the default timezone to UTC:
# settings.py
USE_TZ = True
TIME_ZONE = "UTC" # Store everything in UTC
2. Add a timezone
Field to the Custom User
Model
Use a function for dynamic timezone choices, so you don’t need new migrations when the list changes.
def get_timezone_choices():
import zoneinfo
return [(tz, tz) for tz in sorted(zoneinfo.available_timezones())]
class User(AbstractUser):
# ...
timezone = models.CharField(
_("Timezone"), max_length=50, choices=get_timezone_choices, default="UTC"
)
3. Detect Timezone on the Frontend
Add hidden fields in your Login and Signup forms to capture the user’s timezone from their browser:
document.addEventListener('DOMContentLoaded', function () {
const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
const timezoneInput = document.getElementById('id_timezone');
if (timezoneInput) {
timezoneInput.value = userTimezone;
}
});
You can also let users change their timezone manually in account settings.
4. Use a Custom DateTime Field in Forms
This field will convert datetimes between UTC and the user’s local timezone:
import datetime
from zoneinfo import ZoneInfo
from django import forms
from django.utils import timezone
from django.utils.dateparse import parse_datetime
class TimezoneAwareDateTimeField(forms.DateTimeField):
widget = forms.DateTimeInput(attrs={"type": "datetime-local"})
def __init__(self, user_timezone=None, *args, **kwargs):
self.user_timezone = user_timezone
super().__init__(*args, **kwargs)
def prepare_value(self, value):
if value and self.user_timezone:
try:
user_tz = ZoneInfo(self.user_timezone)
if timezone.is_aware(value):
value = value.astimezone(user_tz)
except Exception:
pass
return value
def to_python(self, value):
if value in self.empty_values:
return None
if isinstance(value, datetime.datetime):
result = value
elif isinstance(value, datetime.date):
result = datetime.datetime(value.year, value.month, value.day)
else:
try:
result = parse_datetime(value.strip())
except ValueError:
raise forms.ValidationError(
self.error_messages["invalid"], code="invalid"
)
if not result:
result = super(forms.DateTimeField).to_python(value)
if result and self.user_timezone:
try:
user_tz = ZoneInfo(self.user_timezone)
if timezone.is_naive(result):
result = result.replace(tzinfo=user_tz)
result = result.astimezone(ZoneInfo("UTC"))
except Exception:
pass
return result
The type="datetime-local"
widget uses the browser’s native date/time picker.
Use the custom field like this:
from django import forms
from django.utils.translation import gettext_lazy as _
from myproject.apps.core.form_fields import TimezoneAwareDateTimeField
from .models import Post
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ["title", "content", "published_from"]
def __init__(self, request, *args, **kwargs):
super().__init__(*args, **kwargs)
self.request = request
self.fields["published_from"] = TimezoneAwareDateTimeField(
label=_("Published from"),
help_text=_("Enter date and time in your local timezone."),
required=False,
user_timezone=self.request.user.timezone,
)
5. Output Dates and Times in User's Timezone
{% load tz %}
{% with user_timezone=request.user.timezone|default:"UTC" %}
{{ post.published_from|timezone:user_timezone|date:"j M, Y H:i" }}
{% endwith %}
Other Options
You can also detect the visitor’s timezone in JavaScript and send it via Ajax to be saved in the Django session. Then you can use it even for anonymous users.
Final Words
Timezones aren’t so scary if you follow Django’s best practices:
- Store all times in UTC.
- Update
tzdata
regularly. - Use the user's timezone only at input/output stages.
- Detect the user’s timezone via JavaScript—no need to ask them manually.
This keeps your website accurate, user-friendly, and ready for global audiences.
Cover photo by Andrey Grushnikov
Also by me
Django Paddle Subscriptions app
For Django-based SaaS projects.
Django GDPR Cookie Consent app
For Django websites that use cookies.