TL;DR: To support multiple timezones in your Django project, you need a way to request your users' specific timezones and create a middleware that uses
django.timezone.activate(user_tz)to enable a specific timezone for a user globally on your site. This ensures that every use of
django.timezone.now()uses the activated timezone. If you prefer reading code directly, see middleware.py and models.py.
Throughout this article, I'll guide you on setting up multi-timezone support in a Django project. This post is aimed at beginners (assuming basic Django knowledge) and intermediates. If what I'm writing seems blatantly obvious to you, you're likely not in one of these categories, so please bear with us.
Time is universally a challenging subject to deal with, especially in software engineering. Handling time zones correctly is hard. Luckily for us, when working with Django, a significant portion of the work has already been done.
For the longest time (literally until yesterday), I thought that the piece of code below was enough to have timezones fully managed and working in Django:
# settings.py TIME_ZONE = "UTC" # Sometimes, I'd switch this to my primary audience's timezone. USE_TZ = True
It turns out I was wrong, or at least I was missing the full picture. There's a bit more work left if you want to consider every user's timezone. Let's explore what's left to do.
Note The official Django documentation on timezones is very well written, so I highly suggest you read it. However, this article will provide you with the essentials, at least that's what I hope.
First, let's start with the basics. In Python, there are two types of datetime objects: naive and aware. We'll keep it simple. Naive datetime objects hold no timezone information, while aware datetimes hold timezone information.
When you have the
USE_TZ setting set to
True in your project settings (as shown in the snippet above), Django will ensure that all the DateTime objects you create are timezone-aware. This is, of course, assuming that you use Django's timezone module to create your dates.
from django.utils import timezone now = timezone.now()
This works if you're not concerned about user-specific timezones, and you're not doing anything fancy with datetime. But what if you are?
Let me paint a picture: I'm building an app where I need to send daily notifications to users at specific times. My project's timezone is set to
UTC – the recommended practice by most of the Django community. Here's the twist: I live in a UTC+1 timezone, and I realized my notifications were coming in an hour late. My cousin, who lives in Europe at a different timezone, also uses my app. Changing the default timezone wasn't an option. My cousin and I, along with potentially many other users, needed to navigate the app seamlessly without time feeling off. Scheduling and time management are at the core of my app, and I couldn't afford to mess that up.
So, I decided to dive deep into understanding how Django deals with time zones – something I'd never made a priority before.
After reading and re-reading the Django documentation, things started to click. First, let's grasp the fundamentals and then dive into setting up our timezone magic.
There are two important concepts that Django uses: the "default time zone" and the "current time zone."
- The default time zone is the timezone you set in your Django settings via
- The current time zone is the timezone used for rendering. It's the one in which your users will browse your site.
current time zone defaults to the
default time zone unless you activate the user's specific timezone using
Does it start to make sense? Django, by default, doesn't know a user's timezone. It's not typically available in request data, so it sticks with the default timezone. You need to ask your users for their time zone – a simple form does the trick – and then manually activate it. The easiest and probably the best way to do this is through a middleware.
Setting Up Multi-Timezone Support
Now, let's dive into a simple example. We'll create a user
Profile model to collect and store each user's timezone. For simplicity's sake, I'll leave out the non-essential parts of the code.
Here's a model for our users' profiles:
import zoneinfo TIMEZONES_CHOICES = [(tz, tz) for tz in zoneinfo.available_timezones()] class Profile(TimeStampedModel): user = models.OneToOneField( "users.User", related_name="profile", on_delete=models.CASCADE ) timezone = models.CharField( verbose_name=_("Fuseau horaire"), max_length=50, default="UTC", choices=TIMEZONES_CHOICES, )
With the model above, we have a simple
CharField to hold the user's timezone and a choices field to render a select using a Django form. You can customize the list of available timezones obtained from the
zoneinfo module. For a simpler user experience, you might choose to filter it by continent or display the city as the label.
Tip: If you're displaying a long list of timezones, consider using a select element with a search feature(e.g, tom-select) for a better user experience.
You'll need to create a form and a view to set the timezone value – standard Django stuff.
Now, let's create the middleware to activate the user's timezone:
import zoneinfo from django.utils import timezone from .models import Profile class TimezoneMiddleware: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): if request.user.is_authenticated: try: profile = request.user.profile except Profile.DoesNotExist: timezone.deactivate() else: timezone.activate(zoneinfo.ZoneInfo(profile.timezone)) else: timezone.deactivate() return self.get_response(request)
This middleware activates the user's timezone if they're authenticated and have a profile. In other cases, we call
deactivate, which sets the timezone to the default. While this last step is not strictly required, that's how the Django docs suggest doing it, so let's stick with that. To complete the setup, don't forget to register your middleware in your settings within the
MIDDLEWARE = [ ... "your_app.middleware.TimezoneMiddleware", ]
With this setup, we've made sure that every call to
timezone.now will takes the user's specific timezone into account. When datetime objects are saved to the database, they are automatically converted to UTC. For example, in my case (UTC+1), if I input "6:00 pm" on the UI to record a time, it will be saved in the database as
5:00 pm (though it will still be rendered as "6:00 pm" to me on the frontend).
Quick Tips and bits:
If you need to create a timezone-aware datetime object manually, for example by combining a date and time, here's how you do it:
import datetime as dt naive_datetime = dt.datetime.combine(my_date, my_time) aware_datetime = timezone.make_aware(naive_datetime, zoneinfo.ZoneInfo(user_timezone))
The code below will generate a new migration every time you run
python manage.py makemigrations:
TIMEZONES_CHOICES = [(tz, tz) for tz in zoneinfo.available_timezones()] class Profile(models.Model): timezone = models.CharField( verbose_name=_("Fuseau horaire"), max_length=50, default="UTC", choices=TIMEZONES_CHOICES, )
The culprit line is
choices=TIMEZONES_CHOICES. A simple fix is to update the migrations file to use the
TIMEZONES_CHOICES constant directly:
class Migration(migrations.Migration): ... operations = [ ... migrations.AddField( model_name="profile", name="timezone", field=models.CharField( choices=TIMEZONES_CHOICES, default="UTC", max_length=50, ), ), ]
There you have it! Your Django app is now fully equipped to support multiple timezones. I hope this article adds a touch of timezone magic to your Django projects. Thanks for the read.