In a Django project with USE_TZ enabled, why use django.utils.timezone.now() instead of datetime.datetime.now()?
answer
- aware versus naive
- what the database stores
- follows the USE_TZ setting
- RuntimeWarning on save
basics
~20 sWith USE_TZ on, timezone.now() returns an aware datetime in UTC, matching how Django stores datetimes. datetime.now() is naive local time: saving it makes Django warn and assume TIME_ZONE, and comparing it with aware values raises TypeError.
solid answer
~40 s`USE_TZ` is `True` by default in current Django, which means datetimes are stored in UTC and handled as aware objects. `timezone.now()` is `datetime.now(tz=UTC)` in that case, and a naive value when `USE_TZ` is off, so it always matches the project's setting. `datetime.now()` returns a naive wall-clock reading. Saving it to a `DateTimeField` emits a `RuntimeWarning` and Django interprets it in the default time zone, `TIME_ZONE`, which is ambiguous in the DST fall-back hour. Comparing it with an aware value loaded from the database raises `TypeError`. So I use `timezone.now()` for the current instant and `localtime()`/`localdate()` when I need the user's wall clock.
code
python · 8 linesfrom django.utils import timezone
from appointments.models import Appointment
upcoming = Appointment.objects.filter(starts_at__gte=timezone.now())
# datetime.datetime.now() here would be naive: RuntimeWarning, interpreted in TIME_ZONE,
# and `appointment.starts_at < datetime.now()` would raise TypeError.go deeper
Recall that USE_TZ is on by default, that timezone.now() gives an aware UTC datetime, and that datetime.now() gives a naive one.
Explain what Django does with a naive value on save: the RuntimeWarning, make_aware with TIME_ZONE, and the TypeError when comparing with aware values.
Show how naive values sneak in from external data and tests, and how to make the RuntimeWarning fail loudly so they are caught early.
Treat UTC-aware datetimes as a codebase-wide invariant, enforced by review, warnings-as-errors in tests and clear conversion points at the edges.
## The setting behind the question `USE_TZ` switches on Django's **time zone support**. It defaults to `True` in `global_settings.py` (since Django 5.0) and `startproject` writes `USE_TZ = True` as well. With it on, Django's contract is: - datetimes are **stored in UTC** in the database; - code works with **aware** datetimes (ones that carry a `tzinfo` and therefore name an exact instant); - values are converted to a human's local time only at the edges: template rendering and form input. A **naive** datetime has no `tzinfo`: it is a wall-clock reading that does not say which clock. What exactly makes a Python datetime aware is a Python-language topic; here the question is what Django does with each kind. ## What the two calls return | Call | With `USE_TZ = True` | With `USE_TZ = False` | |---|---|---| | `django.utils.timezone.now()` | aware, in UTC | naive, local time | | `datetime.datetime.now()` | naive, the process's local wall time | naive, local time | `timezone.now()` is literally `datetime.now(tz=UTC if settings.USE_TZ else None)`, so it follows the project's setting instead of hard-coding one behaviour. That is the first reason to prefer it: the same code is correct under either setting. ## What goes wrong with datetime.now() When a naive value reaches a `DateTimeField` while `USE_TZ` is on, Django: 1. emits `RuntimeWarning: DateTimeField Appointment.starts_at received a naive datetime (...) while time zone support is active.`; 2. **guesses** its zone by attaching the **default time zone** (`TIME_ZONE`) with `make_aware()`; 3. stores the resulting instant in UTC. Django also sets the process's `TZ` environment variable to `TIME_ZONE`, so on most servers `datetime.now()` *is* wall time in that zone and the guess lands on the right instant. The problems are at the edges: - **DST fall-back.** One wall-clock hour happens twice a year; a naive reading from that hour cannot say which one, and Django's guess may be an hour off. - **Comparisons.** Values read back from the database are aware. Comparing or subtracting them against a naive `datetime.now()` raises `TypeError`, so `if appointment.starts_at < datetime.now():` crashes. - **Hidden coupling.** The code is only right as long as the process zone and `TIME_ZONE` agree, which is not true in every environment. - **Noise.** The `RuntimeWarning` floods logs and test output, hiding real problems. ## The right tools - `timezone.now()` for "the current instant". - `timezone.localtime()` to view an aware value in the **current time zone** (see per-request activation), and `timezone.localdate()` for "today" in that zone. - `timezone.make_aware(naive, tz)` when a naive value from an external source must be interpreted in a known zone. - For a model default, pass the callable `default=timezone.now`, not `timezone.now()`, so it is evaluated per row; field options themselves belong to the model-field topic. ## Naive values from outside the project Not every naive datetime comes from `datetime.now()`. They also arrive from CSV imports, legacy databases, third-party APIs that send local timestamps, and hand-written test data. The rule is the same for all of them: decide which zone the value was recorded in and make it aware **before** it reaches the ORM. - A clinic's export in Lisbon local time: `timezone.make_aware(value, zoneinfo.ZoneInfo("Europe/Lisbon"))`. - A Unix timestamp: `datetime.datetime.fromtimestamp(ts, tz=datetime.UTC)`, which the Django docs recommend over the naive form. - Test fixtures: write aware values, or the suite fills with the same `RuntimeWarning`. Converting once at the boundary keeps every value inside the project aware, so the rest of the code never has to guess. ## Worked example: appointment reminders ```python from datetime import timedelta from django.utils import timezone from appointments.models import Appointment soon = timezone.now() + timedelta(hours=24) due = Appointment.objects.filter(starts_at__lte=soon, reminded=False) ``` Every value here is aware UTC, so the comparison happens between exact instants no matter where the patient, the clinic or the server is. Swapping in `datetime.now()` would still run, but with a warning per query parameter and a one-hour error window around DST changes. ## What an interviewer listens for - That `USE_TZ` is on by default in current Django and that storage is UTC. - That `timezone.now()` returns an aware UTC value, not the user's local time. - That Django's fallback for naive values is the **default** zone (`TIME_ZONE`), not the visitor's zone.
- Which time zone does Django assume for a naive datetime saved to a DateTimeField while USE_TZ is on?The default time zone from the `TIME_ZONE` setting, attached with `make_aware()` after a `RuntimeWarning`. It is not the currently activated zone of the request, so a naive value built from a user's local input is misread whenever the user's zone differs from `TIME_ZONE`.
- What does timezone.now() return when USE_TZ is False?A naive datetime in local time, because it calls `datetime.now(tz=None)` in that case. That is why code written against `timezone.now()` keeps working if a legacy project still runs with time zone support off.
saying these in an interview costs you the question
- timezone.now() returns the current user's local time
- USE_TZ is off by default in new Django projects
- Naive datetimes are interpreted in the request's current time zone
- datetime.now() and timezone.now() are interchangeable once USE_TZ is on
- Django stores datetimes in TIME_ZONE rather than UTC