← 回到手记
代码

别再用 utcnow():Python 时区正确写法(3.11+)

Python 3.12 起 datetime.utcnow() 已弃用。本文说明用 datetime.now(UTC) 与 zoneinfo 做 aware 时间,以及存储 UTC、展示上海时区的写法。

很多人写后端日志、接口返回时间,习惯用 datetime.utcnow()。它能跑,但在 Python 3.12 起已经弃用,将来会删掉;更关键的是,它返回的是没有时区信息的 naive 对象,很容易和本地时间搞混。

本文用标准库说明:为什么要改、怎么改,以及怎么在上海时区显示。示例在 Python 3.13.5 上跑通;写法适用于 3.11+(datetime.UTC 别名自 3.11 起可用;zoneinfo 自 3.9 起在标准库)。若你还在 3.10,可以把文中的 UTC 换成 timezone.utc,思路一样。

要解决的问题

后端常见三类需求:

  1. 记下「现在是几点」:写库、打日志、签发过期时间。
  2. 在不同时区之间换算:库里存 UTC,页面上显示 Asia/Shanghai。
  3. 比较、相减两个时间:超时判断、间隔计算。

如果继续用 naive 的 utcnow(),表面上「像是 UTC」,对象上却没有 tzinfo。官方文档明确提醒:很多 datetime 方法会把 naive 对象当成本地时间处理,所以更稳妥的做法是用带时区的 aware 对象表示 UTC。这一点一旦在日志、数据库、缓存之间混用,排查成本会陡增。

核心概念

先记三个点:

  • naive vs aware:tzinfo is None 就是 naive;带了时区就是 aware。两者不能直接相减,会抛 TypeError。
  • datetime.utcnow()(已弃用):返回当前 UTC 的墙钟数字,但 tzinfo 仍是 None。Python 3.12 起触发 DeprecationWarning。
  • 推荐写法:datetime.now(datetime.UTC)(或 timezone.utc)。这是 aware 的 UTC 时间。需要城市时区时用标准库 zoneinfo.ZoneInfo。

官方「What's New in Python 3.12」写明:弃用 utcnow() / utcfromtimestamp(),改用传入 datetime.UTC 的 now() / fromtimestamp()。文档措辞强调的是「用 timezone-aware 对象表示 UTC」,而不只是换一个函数名。

示例代码

取当前 UTC,并转成上海时间展示:

from datetime import datetime, UTC
from zoneinfo import ZoneInfo

# 推荐:aware UTC(3.11+ 可用 UTC 别名;也可用 timezone.utc)
now_utc = datetime.now(UTC)
print(now_utc.isoformat())
# 例如:2026-10-22T04:09:51.100742+00:00

# 展示给上海用户
shanghai = ZoneInfo("Asia/Shanghai")
now_sh = now_utc.astimezone(shanghai)
print(now_sh.isoformat(), now_sh.tzname())
# 例如:2026-10-22T12:09:51.100742+08:00 CST

# 从时间戳构造(替代已弃用的 utcfromtimestamp)
ts = now_utc.timestamp()
from_ts = datetime.fromtimestamp(ts, tz=UTC)
assert from_ts.tzinfo is not None

业务里更稳妥的习惯通常是:

  1. 存储、计算用 UTC aware;
  2. 展示时再 astimezone(ZoneInfo(...));
  3. 序列化优先用带偏移的 ISO 字符串,例如 isoformat();读回来用 datetime.fromisoformat(...)(带偏移时会得到 aware 对象)。

本地验证时,naive 与 aware 相减会失败,这是好事——逼你统一约定:

from datetime import datetime, UTC

try:
    datetime.now() - datetime.now(UTC)
except TypeError as e:
    print(e)
# can't subtract offset-naive and offset-aware datetimes

在本机用 python3 -W default 调用 datetime.utcnow(),会看到弃用警告,大意是:请改用 timezone-aware 对象,例如 datetime.datetime.now(datetime.UTC)。

常见误区

  1. 把 now(UTC) 当成 utcnow() 的「同款替换」。前者是 aware,后者是 naive。接口若长期返回 naive,突然改成 aware,调用方一比较就可能 TypeError。迁移时要一起改比较与序列化,或阶段性明确约定。
  2. 用 replace(tzinfo=...) 给 naive「贴标签」却不做换算。replace 只改元数据,不移动时钟;真正换时区用 astimezone。把本地墙钟标成 UTC 是经典错法。
  3. 在 Windows 等环境依赖系统时区数据。zoneinfo 会找系统 IANA 数据,没有则回退到 PyPI 的 tzdata。跨平台项目建议声明 tzdata 依赖(见 zoneinfo 文档)。
  4. 日志里只打无偏移的字符串。少了 +00:00 / +08:00,后人很难判断那是 UTC 还是服务器本地时间。

小结

  • Python 3.12 起不要再写新的 datetime.utcnow();用 datetime.now(UTC)。
  • 表示真实世界里的「某一瞬间」,优先用 aware 时间;库内 UTC、展示再转本地。
  • 城市时区用 zoneinfo.ZoneInfo("Asia/Shanghai"),换算用 astimezone。

适用版本:示例基于 Python 3.13.5;datetime.UTC 需 3.11+,更早可用 timezone.utc。zoneinfo 需 3.9+。

参考资料

  1. What’s New In Python 3.12(datetime 弃用说明)
  2. datetime — Basic date and time types
  3. zoneinfo — IANA time zone support
  4. Deprecations(Python 文档)
✳

谢谢你读到这里。