datetime.now()和datetime.utcnow()返回naive时间,跨时区或夏令时场景不可靠;正确做法是显式绑定时区(如zoneinfo或pytz.localize()),存储和计算统一用utc。

datetime.now() 和 datetime.utcnow() 的时区幻觉
这两个函数看起来是“当前时间”的标准答案,但它们默认返回 naive datetime(无时区信息),一旦涉及跨时区或夏令时切换,结果就不可靠。比如在纽约,3 月第二个周日凌晨 2 点会跳到 3 点——datetime.now() 不知道这个跳变,它只是机械地读系统时钟。
- 永远别用
datetime.now()做跨日期计算或存储,它不带tzinfo,等于没身份证 -
datetime.utcnow()更危险:它假装“UTC”,但实际仍是naive,和真正 UTC 时间可能差出一整小时(尤其在夏令时期间) - 正确做法是显式绑定时区:
datetime.now(tz=ZoneInfo("America/New_York"))(Python 3.9+)或用pytz的localize()(注意:不能直接赋值tzinfo=)
pytz.localize() vs pytz.convert() 的误用高频点
pytz 是老项目常用库,但它对 localize() 和 astimezone() 的行为设计反直觉:给一个 naive datetime 加时区必须用 localize();而把一个 aware datetime 转成另一时区,必须用 astimezone()。用错就会得到错误的偏移量。
- 错误示范:
dt.replace(tzinfo=pytz.timezone("Europe/London"))—— 这会硬塞一个固定偏移,无视夏令时规则 - 正确流程:先
localize()得到本地时间的 aware 对象,再astimezone()转目标时区 - 注意
pytz的timezone().localize()在夏令时边界(如 3 月/11 月)会自动选 DST 或 STD 偏移,但前提是传入的naive datetime确实落在该时区合法范围内
ZoneInfo(Python 3.9+)里 DST 切换日的“模糊时间”处理
每年 11 月第一个周日凌晨 2 点,美国多数地区会把钟拨回 1 小时,导致 1:00–1:59 出现两次。这时 ZoneInfo 默认按“第二次出现”(即标准时间)解析,但如果你明确想取第一次(DST 时间),就得手动指定 is_dst 参数。
- 默认行为:
datetime(2023, 11, 5, 1, 30).replace(tzinfo=ZoneInfo("America/Chicago"))→ 解析为 CST(UTC-6) - 强制 DST:
datetime(2023, 11, 5, 1, 30, fold=0).replace(tzinfo=ZoneInfo("America/Chicago"))(fold=0表示第一次) -
fold是 Python 3.6 引入的属性,ZoneInfo依赖它做歧义消解;不用fold或用错值,会导致时间偏移错误
timedelta 计算跨 DST 边界时的“隐形偏移”
用 timedelta(days=1) 给一个带时区的 datetime 加一天,看似安全,但在 DST 开始/结束日,结果可能不是“24 小时后”,而是 23 或 25 小时后——因为底层按本地日历日推进,而非绝对秒数。
立即学习“Python免费学习笔记(深入)”;
- 例如:在洛杉矶,3 月 12 日 12:00 PST(UTC-8) +
timedelta(days=1)= 3 月 13 日 12:00 PDT(UTC-7),实际间隔是 25 小时 - 如果业务逻辑依赖“精确 24 小时后”,必须用 UTC 时间做加减:
dt.astimezone(ZoneInfo("UTC")) + timedelta(hours=24),再转回本地时区 - 数据库存时间建议统一用 UTC(
datetime(..., tzinfo=ZoneInfo("UTC"))),所有加减、比较都在 UTC 下做,避免本地时区的 DST 干扰
夏令时不是边缘情况,而是真实世界的时间常态。最易被忽略的是:系统时区设置、数据库时区配置、前端 JS Date 对象的本地解析,三者只要有一个没对齐,时间就悄悄错位。别信“看起来对”,得查 tzinfo、看 utcoffset()、验 dst() 返回值。










