​编辑 Python datetime 时区的七个坑:naive 和 aware 相等永远为 False、utcnow 差 8 小时、pytz 冒出 +08:06

发布时间:2026/10/7 8:31:49
​编辑 Python datetime 时区的七个坑:naive 和 aware 相等永远为 False、utcnow 差 8 小时、pytz 冒出 +08:06 Python 的 datetime 有个很阴的设计同一个类型既可以带时区aware也可以不带naive两种对象长得一样、方法一样大多数时候混着用也不报错。等它出问题的时候通常是某个定时任务早跑了八个小时或者报表里凭空多出一天。下面七个坑都是实跑出来的环境 Python 3.14.6机器本地时区是 Asia/ShanghaiUTC8。坑 1naive 和 aware 比大小报错比相等却静默返回 Falsefrom datetime import datetime, timezone, timedelta from zoneinfo import ZoneInfo sh ZoneInfo(Asia/Shanghai) a datetime(2026, 9, 30, 10, 0) # naive b datetime(2026, 9, 30, 10, 0, tzinfosh) # aware a b # TypeError: cant compare offset-naive and offset-aware datetimes a b # False比大小会直接抛 TypeError这个算友好。真正危险的是它不报错永远返回 False。代码里写if record.updated_at last_sync:这种判断只要一边带时区一边不带就永远走不进去而且没有任何提示。建议在系统边界读数据库、解析请求、读配置统一转成 aware内部只流通 aware 对象。坑 2utcnow() 返回的是 naivetimestamp() 会把它当本地时间import time u datetime.utcnow() round(u.timestamp() - time.time()) # -28800 round(datetime.now(timezone.utc).timestamp() - time.time()) # 0utcnow()返回的数值是 UTC 时间但对象本身是 naive 的。timestamp()遇到 naive 对象一律按本地时区解释于是在东八区机器上差了 28800 秒正好 8 小时。这个 API 从 3.12 开始已经标记废弃调用会触发DeprecationWarning。替换成datetime.now(timezone.utc)即可。utcfromtimestamp()同理换成fromtimestamp(ts, tztimezone.utc)。坑 3replace(tzinfo) 和 astimezone() 不是一回事d datetime(2026, 9, 30, 10, 0, tzinfotimezone.utc) d.replace(tzinfosh).isoformat() # 2026-09-30T10:00:0008:00 d.astimezone(sh).isoformat() # 2026-09-30T18:00:0008:00replace只是换标签墙上时间不变表示的已经是另一个时刻早了 8 小时astimezone才是把同一个时刻换算到另一个时区。什么时候用 replace只有一种情况你手上是一个 naive 时间并且确定它本来就是某个时区的墙上时间只是缺了标签。其余情况都应该用 astimezone。坑 4pytz 直接塞进构造函数得到 08:06import pytz datetime(2026, 9, 30, 10, tzinfopytz.timezone(Asia/Shanghai)).isoformat() # 2026-09-30T10:00:0008:0608:06 不是笔误。pytz 的时区对象默认取的是该地区历史上第一个偏移也就是 1901 年前的地方平时LMT。pytz 要求必须用tz.localize(dt)直接传给tzinfo就会中招。Python 3.9 起标准库有了zoneinfo可以直接放进构造函数行为是对的。新项目没必要再引入 pytz老项目里看到tzinfopytz.timezone(...)基本都是 bug。坑 5解析字符串时时区信息说丢就丢datetime.fromisoformat(2026-09-30T02:00:00Z).isoformat() # 2026-09-30T02:00:0000:00 datetime.strptime(2026-09-30 10:00:00, %Y-%m-%d %H:%M:%S).tzinfo # None好消息是 3.11 起fromisoformat认识结尾的Z了老版本会报错。坏消息是只要字符串里本来就没有偏移解析出来就是 naive后面所有运算都会按坑 1、坑 2 的方式出错。接口约定时间格式时尽量要求带偏移的 ISO 860108:00或Z或者干脆传 Unix 时间戳。如果对方只给了2026-09-30 10:00:00要在文档里问清楚这是哪个时区的墙上时间然后在解析那一行就replace(tzinfo...)补上标签。坑 6有夏令时的时区加 24 小时不等于加一天ny ZoneInfo(America/New_York) x datetime(2026, 11, 1, 0, 30, tzinfony) y x timedelta(hours24) x.isoformat() # 2026-11-01T00:30:00-04:00 y.isoformat() # 2026-11-02T00:30:00-05:00 y.astimezone(timezone.utc) - x.astimezone(timezone.utc) # 1 day, 1:00:002026 年 11 月 1 日美国结束夏令时。对 aware 对象做加法Python 是在墙上时间上加的墙上时间刚好过了 24 小时但实际流逝了 25 小时。国内业务一般碰不到因为中国没有夏令时。但只要你的系统服务海外用户或者要按用户所在时区算「每天几点提醒」就一定会遇到。算「经过了多久」时先转成 UTC 再相减。坑 7fromtimestamp 默认给本地时间换台机器结果就变ts 1790733600 datetime.fromtimestamp(ts) # 2026-09-30 10:00:00 本机东八区 datetime.fromtimestamp(ts, tztimezone.utc).isoformat() # 2026-09-30T02:00:0000:00不传tz时结果取决于运行这段代码的机器时区。开发机是东八区Docker 容器默认往往是 UTC同一段代码在本地和线上差 8 小时。容器里设置TZ环境变量能缓解但更稳的做法是代码里永远显式传tz。一套简单的约定1. 存储和传输只用 UTC数据库字段、消息队列、接口。2. 程序内部只流通 aware 对象naive 在入口处就处理掉。3. 只在展示给用户的那一刻用astimezone转成用户所在时区。4. 禁用utcnow()、utcfromtimestamp()、不带tz的now()和fromtimestamp()可以在 lint 里加规则。我自己在小站 forxi.cn 上写定时类的功能时就是按这四条来约束代码的坑 7 那种「本地好好的、进了容器差 8 小时」的问题基本就不会再出现。说一句局限上面的约定适合「时刻」类数据下单时间、日志时间。像生日、节假日、「每天早上九点」这种和具体时刻无关的日期或墙上时间硬转成 UTC 反而会出问题应该存成 date 或者「时间 时区名」另外处理。