Why is datetime.datetime.utcnow() deprecated, and what replaces it?
answer
- Right numbers, missing label
- The result was not tagged at all
- Untagged values are read as local, not UTC
- Deprecated in Python 3.12
- now(datetime.timezone.utc) is the replacement
basics
~10 sIt returned UTC wall-clock fields with tzinfo left as None, so the value looked tagged but was naive. Python 3.12 deprecated it; use datetime.datetime.now(datetime.timezone.utc), which returns an aware object.
solid answer
~40 s`datetime.datetime.utcnow()` gave you the right fields with no label: a **naive** object whose values happened to be UTC. That is the worst case, because everything downstream that meets an untagged value assumes **local** time — `astimezone()` shifts it, `timestamp()` converts it as local, and comparing it against an aware value raises `TypeError` for ordering or silently returns `False` for equality. Python **3.12** deprecated it along with `datetime.datetime.utcfromtimestamp()`; both still exist in 3.14 and emit a `DeprecationWarning`. Replace them with `datetime.datetime.now(datetime.timezone.utc)` and `datetime.datetime.fromtimestamp(ts, datetime.timezone.utc)`, which return aware values. Note that `DeprecationWarning` is hidden by default outside `__main__`, so promote it to an error in your test run to find the remaining calls.
code
python · 10 linesimport datetime
import warnings
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
legacy = datetime.datetime.utcnow()
print(type(caught[0].message).__name__)
print("legacy tzinfo:", legacy.tzinfo)
print("replacement tzinfo:", datetime.datetime.now(datetime.timezone.utc).tzinfo)go deeper
Know the replacement by heart: datetime.datetime.now(datetime.timezone.utc) instead of datetime.datetime.utcnow(), and be able to say the old call returned a value with no time zone attached.
Explain why an untagged UTC value is worse than an obviously local one: the standard library reads a naive value as local time, so conversions and epoch calculations silently shift it. Name Python 3.12 as the deprecation release.
Talk about finding and retiring the calls safely: promote DeprecationWarning to an error in tests, migrate the boundary rather than individual call sites, and watch for the mixed-kind window where comparisons start raising or silently answering False.
Own the policy: a single approved way to read the clock, a lint rule that blocks the deprecated pair, and a scheduled response to future removals so a warning never becomes a production AttributeError on an upgrade.
### What `utcnow()` actually returned `datetime.datetime.utcnow()` read the system clock, converted it to UTC, and then returned a **naive** `datetime` — the correct UTC wall-clock fields with `tzinfo` left as `None`. The value was right and the label was missing, which is the worst combination: it looks like a UTC timestamp, prints like a UTC timestamp, and is indistinguishable at runtime from a local-time reading produced by `datetime.datetime.now()`. Everything downstream then has to guess, and the standard library guesses *local*, not UTC: * `astimezone(...)` on the result treats the fields as local time and shifts them, so on a machine whose clock is not UTC the instant silently moves. * `timestamp()` on the result likewise interprets the fields as local time, producing an epoch value wrong by the machine's offset. * Comparing it against a properly aware value raises `TypeError` for ordering, or quietly answers `False` for equality. * Serialising it drops the offset from the output, so consumers cannot recover the intent. The bugs that follow are the classic ones: timestamps off by whole hours, records that sort into the wrong order around a deployment that changed the container's zone, and code that behaves correctly on a CI runner set to UTC and wrongly on a developer laptop. ### The deprecation **Python 3.12** deprecated both `datetime.datetime.utcnow()` and `datetime.datetime.utcfromtimestamp()`. Calling either emits a `DeprecationWarning`. The functions still exist in 3.14 — this is a deprecation, not a removal — but they are on the way out and should not appear in new code. Remember that `DeprecationWarning` is hidden by default outside `__main__`, so a large codebase can call `utcnow()` thousands of times and show nothing. Surface it deliberately: run the test suite with warnings promoted to errors for that category, or configure the warnings filter in the test harness so a new call fails the build. ### The replacements | deprecated | replacement | |---|---| | `datetime.datetime.utcnow()` | `datetime.datetime.now(datetime.timezone.utc)` | | `datetime.datetime.utcfromtimestamp(ts)` | `datetime.datetime.fromtimestamp(ts, datetime.timezone.utc)` | Both replacements return **aware** objects whose `utcoffset()` is a zero `timedelta`. Since Python 3.11 `datetime.timezone.utc` also answers to the shorter alias `datetime.UTC`, so `datetime.datetime.now(datetime.UTC)` is the same call written more briefly. ### The migration is not a blind find-and-replace The two calls do not produce identical objects: the old one is naive and the new one is aware, and that difference propagates. A codebase that swaps `utcnow()` for `now(datetime.timezone.utc)` in one place and leaves naive values elsewhere has just created the mixed comparisons described above — the sort that raises `TypeError`, or worse, the equality that answers `False` and hides. So the change is a boundary migration, not a token substitution: 1. Convert the clock reads first, since those are the source of new values. 2. Tag the values arriving from storage and parsing at the same boundary, deciding explicitly what the stored naive values meant. 3. Look for the places that compare, sort, subtract or key on a timestamp, and confirm both sides are now aware. 4. Add a guard at the edge that rejects a naive value outright rather than defaulting it. ### Why the interviewer asks It separates a candidate who repeats "use `now(datetime.timezone.utc)`" from one who can say *why* the old call was wrong: not that it returned the wrong time, but that it returned an untagged time, and that Python's default interpretation of an untagged time is local, not UTC. The second answer also explains why the migration is riskier than it looks, and why the fix belongs at the process boundary rather than at each call site. ### What did not change `datetime.datetime.now()` with no argument still returns a naive local reading and is **not** deprecated — it is a legitimate call when you genuinely want a local wall-clock value for display. `datetime.datetime.now(tz)` with any `tzinfo` returns an aware value. And `datetime.datetime.utcnow()` remains importable and callable in 3.14; nothing breaks today, which is exactly why the warning has to be surfaced deliberately before a future removal turns a warning into an `AttributeError`. ### A quick self-check for any timestamp helper If a utility in your codebase returns "now", ask two questions of it: does the returned object's `utcoffset()` come back as something other than `None`, and does every caller keep it that way? A helper that returns an aware value which a caller then strips with `replace(tzinfo=None)` — usually to satisfy a storage layer or a serializer that rejects offsets — has reintroduced exactly the condition the deprecation was meant to end, one layer further down. The fix is the same as the original one: keep the offset all the way to the boundary, and if a component genuinely cannot store it, record UTC by convention, name the field so it says so, and tag the value again the moment it is read back.
- Is datetime.datetime.now() deprecated too?No. Bare `datetime.datetime.now()` returns a naive **local** reading, which is a legitimate value when you want local wall-clock time for display, and it is not deprecated. Only the UTC-flavoured pair — `utcnow()` and `utcfromtimestamp()` — was deprecated in 3.12, because their results claimed UTC in name while carrying no offset. `datetime.datetime.now(tz)` with a tzinfo argument returns an aware value and is the recommended form.
- How do you find the remaining utcnow() calls in a large codebase?Grep is a start but misses aliased and dynamic calls, and `DeprecationWarning` is hidden by default outside `__main__`. Configure the warnings filter in the test harness to turn that category into an error, so any exercised call fails the build with a stack trace at the call site. Pair that with a lint rule so new calls never land.
- Can you just swap every utcnow() call for now(datetime.timezone.utc)?Not blindly. The old call returned a naive object and the new one returns an aware object, so a partial swap creates mixed pairs — sorts that raise `TypeError` and equality checks that quietly answer `False`. Treat it as a boundary migration: convert clock reads, then tag values arriving from storage and parsing, then check every place that compares, sorts, subtracts or keys on a timestamp.
saying these in an interview costs you the question
- Says utcnow() returned an aware UTC datetime
- Claims utcnow() was removed in Python 3.12
- Thinks the deprecation was about accuracy or performance
- Assumes an untagged datetime is interpreted as UTC
- Swaps the calls one by one with no boundary plan
- Relies on default warning output to find the call sites