With USE_TZ on, what is the difference between Django's TIME_ZONE setting and the current time zone, and what uses each?
answer
- storage, default, current
- UTC in the database
- activate() per thread
- templates and forms convert at the edges
basics
~20 sDjango stores datetimes in UTC. TIME_ZONE is the default zone: the fallback and the assumption for naive values. The current time zone, set with timezone.activate() per request, is what templates render in and forms parse input in.
solid answer
~30 sWith `USE_TZ` on, storage is always UTC. `TIME_ZONE` defines the **default** time zone: it is used whenever nothing has been activated, it is the zone Django assumes for naive datetimes (with a `RuntimeWarning`), and the process's `TZ` is set to it. The **current** time zone is whatever `timezone.activate()` set for this thread, typically from the user's profile in a middleware, because Django has no header to infer it from. Templates convert aware datetimes to the current zone when rendering, `forms.DateTimeField` interprets input in it, and `__date`-style lookups evaluate in it. The global default for `TIME_ZONE` is `America/Chicago`, but `startproject` writes `UTC`.
code
python · 6 linesUSE_TZ = True # the default since Django 5.0
TIME_ZONE = "UTC" # default zone: fallback for rendering and for naive values
# elsewhere, per request:
# from django.utils import timezone
# timezone.activate("Europe/Lisbon") # current zone for this threadgo deeper
Recall that storage is UTC, TIME_ZONE is the default zone, and timezone.activate() sets the current zone used for display.
Explain which operations use the current zone (templates, form parsing, date-part lookups) and which use the default (naive values, nothing activated).
Diagnose display and grouping bugs by asking which zone was active where, especially in background jobs that never activate one.
Set the policy: UTC as TIME_ZONE, a stored per-user zone, one activation point per entry path, and conversions only at the edges.
## Three zones, three jobs With `USE_TZ = True`, Django keeps three ideas apart. Mixing them up is the source of most time-zone bugs in Django code. | Concept | Where it comes from | Used for | |---|---|---| | **UTC** | fixed | storage in the database and `timezone.now()` | | **default time zone** | the `TIME_ZONE` setting | the fallback current zone; interpreting naive datetimes; the process's `TZ` variable | | **current time zone** | `timezone.activate()` for this thread or task, else the default | rendering in templates, parsing form input, `localtime()`, `__date`-style lookups | ## Storage: always UTC - Django writes aware datetimes to the database as UTC and reads them back as aware values. - On PostgreSQL, columns are `timestamp with time zone` and the connection runs in UTC; other backends store the UTC value without an offset. - The database never records the user's zone, so a stored instant does not remember who created it or where. ## The default time zone: TIME_ZONE - `TIME_ZONE` defaults to `"America/Chicago"` in `global_settings.py`; `startproject` writes `TIME_ZONE = "UTC"` into new projects, which is what most teams keep. - `timezone.get_default_timezone()` returns it as a `zoneinfo.ZoneInfo`. - It is the **current** zone whenever nothing has been activated. - It is the zone Django **assumes** for a naive datetime saved while `USE_TZ` is on (with a `RuntimeWarning`), and the zone the `tz` template filters assume for naive input. - Django also sets the process's `TZ` environment variable to it, so naive `datetime.now()` calls return wall time in that zone. ## The current time zone: per request - `timezone.activate(tz)` accepts a `tzinfo` or a zone name and sets it for the current thread (or async context); `timezone.deactivate()` removes it, falling back to `TIME_ZONE`; `timezone.override(tz)` does the same for a block and restores the previous value. - Unlike language selection, Django has **no automatic source** for it: there is no header equivalent to `Accept-Language`. Projects store a zone on the user profile or in the session and activate it in their own middleware. - `timezone.get_current_timezone()` returns it; the `tz` context processor exposes its name as `TIME_ZONE` in templates. ## Where conversion happens 1. **Templates.** An aware datetime printed with `{{ appointment.starts_at }}` is converted to the current zone before formatting. 2. **Forms.** A `forms.DateTimeField` parses the user's naive input and makes it aware in the **current** zone, raising a validation error for a wall time that does not exist or is ambiguous there. 3. **Queries.** Date-part lookups and truncations (`__date`, `__year`, `TruncDay` and friends) are evaluated in the current zone unless given an explicit `tzinfo`. 4. **Python code.** Nothing converts automatically; you call `timezone.localtime(value)` when you need the local view. ## A multi-zone appointments example A clinic books appointments for patients in Lisbon, Chicago and Singapore with `TIME_ZONE = "UTC"`: - a patient in Chicago books "10:00" in a form while `America/Chicago` is active, so the form stores the matching UTC instant; - the Singapore receptionist, with `Asia/Singapore` active, sees the same row rendered in Singapore time; - a nightly job with nothing activated runs in `TIME_ZONE`, UTC, and must convert explicitly if it groups by a patient's local day. ## Checking which zone is in effect In a Django shell the difference is easy to see: ```python >>> from django.utils import timezone >>> timezone.get_default_timezone_name() 'UTC' >>> timezone.get_current_timezone_name() # nothing activated yet 'UTC' >>> timezone.activate("Asia/Singapore") >>> timezone.get_current_timezone_name() 'Asia/Singapore' >>> timezone.now().tzinfo # still UTC datetime.timezone.utc ``` The default zone never changes at runtime; the current zone changes per thread with every `activate()`, `deactivate()` or `override()`. ## Common confusions - Setting `TIME_ZONE` to the users' zone does **not** make storage local; it only changes the default display zone and the naive-value guess. - Changing `TIME_ZONE` later does **not** rewrite stored data; stored UTC instants stay correct and only their default rendering changes. - Activating a zone does **not** change `timezone.now()`; it still returns UTC, and only conversions for display, input and date-part lookups follow the current zone.
- What is TIME_ZONE's default in Django's global settings, and why do most projects show UTC?`global_settings.py` sets `TIME_ZONE = "America/Chicago"`, a historical default. The `startproject` template overrides it with `TIME_ZONE = "UTC"`, so projects created with the command start with UTC as the default zone.
- Does changing TIME_ZONE on a live project require a data migration?Not for values stored with `USE_TZ` on, because they are UTC instants and stay correct. What changes is the default rendering zone and the zone assumed for any naive value saved afterwards. Code that relied on the old default, such as jobs grouping by local day, needs review.
An airline runs every timetable in UTC in its operations room, which is the database. Each airport's departure board is the current time zone, converting the same instants for the people standing in front of it. TIME_ZONE is the board hung in any room that has not chosen its own city.
saying these in an interview costs you the question
- TIME_ZONE decides the zone datetimes are stored in
- Django detects each visitor's time zone from a request header
- Templates render datetimes in UTC unless you add a filter
- Changing TIME_ZONE requires rewriting every stored datetime
- Forms interpret typed datetimes in UTC