The settings that turn it on, and what each one actually controls
coreintermediateFour settings decide how Django handles language and time, and they are easy to confuse because two of them look like defaults and are not. `USE_TZ` decides whether datetimes are timezone-aware — it is **on by default**, and it is the setting that makes `timezone.now()` return an aware value in UTC. `TIME_ZONE` is the *default display* timezone, not the storage one; storage is UTC whenever `USE_TZ` is on. `LANGUAGE_CODE` is the fallback language for a request whose own preference could not be determined. `USE_I18N` switches the translation machinery on at all.
Think of it as
The useful split is between what is *stored* and what is *shown*, and each pair of settings sits on one side of it. With `USE_TZ` on, every datetime you save goes to the database in UTC — one unambiguous instant, the same value regardless of who wrote it. `TIME_ZONE` then governs how that instant is rendered when nothing more specific applies, which is why calling it "the project timezone" misleads people into thinking it changes storage. It does not. The same shape holds for language: the strings in your code are one canonical language, and `LANGUAGE_CODE` is only the fallback used for rendering when a request has expressed no preference of its own. Requests almost always do express one. With `LocaleMiddleware` installed, Django resolves the active language per request in a documented order — a language prefix in the URL, then a cookie, then the `Accept-Language` header, then `LANGUAGE_CODE` — and the result lands on `request.LANGUAGE_CODE`. The equivalent for time has no such default: Django will not guess a user's timezone, because a browser does not send one in a header. You have to obtain it, store it against the user or the session, and call `timezone.activate()` per request. Until you do, every timestamp on the page renders in `TIME_ZONE`, which is correct for exactly the subset of your users who happen to live there. The two settings that are usually wrong in an existing project are these last two: `USE_TZ` turned off years ago by someone who found aware datetimes annoying, and a `TIME_ZONE` set to the founder's city and quietly treated as though it were storage.
What we're doing: Activate the right timezone per request from the user's stored preference, and fall back cleanly for everybody else.
- 14
- A stored IANA name on the user is the only reliable source. Guessing from an IP address gets travellers wrong and gets VPN users wrong more often.
- 20
- `timezone.activate()` sets the zone for this thread. Everything after it — template rendering, `localtime()`, form input parsing — uses it, so no template needs to know who the user is.
- 21–22
- A stale or hand-edited zone name must not become a 500. Falling back to `TIME_ZONE` shows a slightly wrong time; raising shows nothing at all.
- 24
- `deactivate()` is the explicit fallback to `TIME_ZONE`, not a no-op. Being explicit is what makes the anonymous-visitor path readable.
- 26–29
- The `finally` matters because workers reuse threads. An activated timezone that is never cleared leaks into whichever request that thread serves next — a bug that only appears under concurrency.
Why this works: Storage stays UTC for everyone, each request renders in the viewer's own zone, and bad or missing data degrades to `TIME_ZONE` instead of an error.
Reading `settings.TIME_ZONE` to decide what a user should see
Wrong
Better
What you see: Timestamps are correct for colleagues in the office and consistently hours out for everyone else, and nobody notices until a customer disputes a deadline.
Why: `TIME_ZONE` is a project-wide fallback, so reading it directly hard-codes the assumption that every reader shares one zone. `timezone.localtime()` uses the *currently active* zone, which the middleware set from the user's own preference and which falls back to `TIME_ZONE` only when there is nothing better. The two agree in development, where you are the only user, which is precisely why the bug survives to production.
- Whole: USE_TZ = True TIME_ZONE = "Europe/London" USE_I18N = True LANGUAGE_CODE = "en-gb"
- USE_TZ = True — Storage: On by default. Datetimes become aware and are stored in UTC. Turning this off does not simplify anything — it moves the ambiguity into your data.
- TIME_ZONE = "Europe/London" — Display default: What a timestamp renders as when nothing more specific is active. It is not where the value is stored, and naming it "the project timezone" is what causes the confusion.
- USE_I18N = True — Machinery switch: Turns the translation layer on. With it off, `gettext` calls return the original string and `.po` files are never consulted.
- LANGUAGE_CODE = "en-gb" — Fallback only: Used when `LocaleMiddleware` finds no URL prefix, no cookie and no usable `Accept-Language` header. Most real requests never reach it.
Storage versus display — which setting is on which side
Together
How the active language and the active timezone are resolved
Together
Remember: Time zone support is on by default. `USE_TZ` controls *storage* — aware datetimes, saved in UTC — while `TIME_ZONE` only controls the default *display* zone; calling it "the project timezone" is what makes people think it changes what is stored. `LANGUAGE_CODE` is likewise a fallback: `LocaleMiddleware` resolves per request via URL prefix → cookie → `Accept-Language` → `LANGUAGE_CODE`. There is no equivalent for time, because no header carries a zone — store it on the user and call `timezone.activate()`, then deactivate in a `finally` so it cannot leak into the next request on that thread.
See also: aware datetimes utc and dst · gettext and the translation workflow · settings py

