处理日期时间是程序员最经常”看起来简单实际处处是坑”的领域——时区、闰秒、夏令时、格式化、跨月加减……任何一个疏忽都能引发生产事故。Python 的 datetime 模块把这些抽象成了几个类:date、time、datetime、timedelta、tzinfo。这一篇讲清楚怎么”对”地用它们,以及现代 Python 的 zoneinfo。
一、四个核心类
date :日期(年月日)
time :时间(时分秒微秒)
datetime :日期时间(date + time)
timedelta :时长(差值)
1 2 3 4 5 6 from datetime import date, time, datetime, timedeltad = date(2025 , 9 , 25 ) t = time(14 , 30 , 0 ) dt = datetime(2025 , 9 , 25 , 14 , 30 , 0 ) delta = timedelta(days=7 , hours=3 )
二、获取当前时间 1 2 3 4 5 6 from datetime import date, datetimedate.today() datetime.now() datetime.utcnow() datetime.now(tz=...)
优先用 datetime.now(tz=...) ,不带时区的对象在跨时区场景会出问题。
三、时间戳 1 2 3 4 5 6 datetime.now().timestamp() datetime.fromtimestamp(1758787815 ) datetime.fromtimestamp(1758787815 , tz=timezone.utc)
四、格式化与解析 strftime = string format time(datetime → 字符串);strptime = string parse time(字符串 → datetime)。
1 2 3 4 5 6 7 8 dt = datetime.now() dt.strftime("%Y-%m-%d %H:%M:%S" ) datetime.strptime("2025-09-25 14:30" , "%Y-%m-%d %H:%M" )
常用格式符
符号
含义
例
%Y
4 位年
2025
%m
月(补零)
09
%d
日(补零)
25
%H
24 小时(补零)
14
%M
分
30
%S
秒
15
%f
微秒
123456
%A
星期全称
Thursday
%a
星期缩写
Thu
%B
月全称
September
%b
月缩写
Sep
%Z
时区名
UTC
%z
时区偏移
+0800
ISO 8601(推荐格式) 1 2 3 4 5 6 7 8 dt.isoformat() datetime.fromisoformat("2025-09-25T14:30:15" ) date.fromisoformat("2025-09-25" )
存到数据库、发到 API 时永远用 ISO 8601 格式 ——无歧义、易解析、跨语言。
五、timedelta:时间差 1 2 3 4 5 6 7 8 9 10 11 12 13 14 from datetime import timedeltadelta = timedelta(days=7 , hours=3 , minutes=30 ) print (delta) print (delta.total_seconds()) now = datetime.now() tomorrow = now + timedelta(days=1 ) last_week = now - timedelta(weeks=1 ) diff = datetime.now() - datetime(2025 , 1 , 1 ) print (diff.days)
注意 :timedelta 支持 days/seconds/microseconds/milliseconds/minutes/hours/weeks,但不支持 months 和 years (因为月长度不定,年可能闰)。要加月/年用 dateutil.relativedelta 或手动。
1 2 3 from dateutil.relativedelta import relativedeltanext_month = datetime.now() + relativedelta(months=1 ) next_year = datetime.now() + relativedelta(years=1 )
六、时区处理(重头戏) Python 3.9+ 内置 zoneinfo(IANA 时区数据库):
1 2 3 4 5 6 7 8 9 10 11 12 13 from datetime import datetime, timezonefrom zoneinfo import ZoneInfoutc_now = datetime.now(tz=timezone.utc) beijing = datetime.now(tz=ZoneInfo("Asia/Shanghai" )) tokyo = datetime.now(tz=ZoneInfo("Asia/Tokyo" )) print (utc_now.isoformat()) print (beijing.isoformat()) utc_now.astimezone(ZoneInfo("America/New_York" ))
准则 :
存储用 UTC (时间戳或 ISO 8601 with +00:00);
展示时转当地时区 ;
拒绝 naive datetime (不带时区的 datetime)参与跨时区比较。
Python 3.8 及更早需要用第三方 pytz 或 python-dateutil。
七、naive vs aware
naive datetime :不带时区信息(tzinfo 是 None);
aware datetime :带时区信息。
1 2 3 4 naive = datetime.now() aware = datetime.now(tz=timezone.utc) naive < aware
规则 :别混用 。要么全 naive(本地时间,只在单机小工具用),要么全 aware(生产系统)。
八、time 模块(相对底层) 除了 datetime,还有 time 模块——更”贴近系统”的接口:
1 2 3 4 5 6 7 8 9 import timetime.time() time.time_ns() time.sleep(2 ) time.monotonic() time.perf_counter() time.strftime("%Y-%m-%d %H:%M:%S" , time.localtime())
测代码耗时 :
1 2 3 4 5 6 import timestart = time.perf_counter() do_stuff() elapsed = time.perf_counter() - start print (f"用了 {elapsed:.4 f} s" )
九、UNIX 时间戳与 datetime 互转 1 2 3 4 5 6 7 8 datetime.fromtimestamp(1758787815 ) datetime.fromtimestamp(1758787815 , tz=timezone.utc) dt.timestamp()
十、日期算术:谨慎处理月和年 1 2 3 4 5 6 7 8 9 10 11 12 13 d + timedelta(days=1 ) from dateutil.relativedelta import relativedeltad + relativedelta(months=1 ) from calendar import monthrangeyear, month = 2025 , 9 _, last_day = monthrange(year, month) month_end = date(year, month, last_day)
跨月边缘的 bug 特别多 ——比如 1 月 31 日加一个月,应该是 2 月 28 还是 3 月 3?不同库处理不同,一定要读文档。
十一、常见操作汇总 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 today_start = datetime.combine(date.today(), time.min ) today_end = datetime.combine(date.today(), time.max ) today = date.today() monday = today - timedelta(days=today.weekday()) first_of_month = today.replace(day=1 ) (date(2025 , 12 , 31 ) - date.today()).days def date_range (start, end ): d = start while d < end: yield d d += timedelta(days=1 ) for d in date_range(date(2025 , 9 , 1 ), date(2025 , 9 , 5 )): print (d)
十二、格式化与国际化 strftime("%A") 输出星期名,取决于 locale:
1 2 3 4 5 6 import localelocale.setlocale(locale.LC_TIME, "zh_CN" ) datetime.now().strftime("%A" ) locale.setlocale(locale.LC_TIME, "en_US" ) datetime.now().strftime("%A" )
f-string 内部也能用格式说明符:
1 f"{datetime.now():%Y-%m-%d %H:%M:%S} "
十三、第三方库
python-dateutil:更强解析、relativedelta;
arrow:更”人性化”的 API;
pendulum:类似 arrow,更完整;
whenever(新兴):类型安全的时区处理。
日常业务用内置 datetime + zoneinfo 就够。特殊需求再上第三方。
十四、一个完整例子:会议时区转换 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 """把一个会议时间显示给多个时区的参与者。""" from datetime import datetimefrom zoneinfo import ZoneInfodef format_for (dt_utc: datetime, tz_name: str ) -> str : tz = ZoneInfo(tz_name) local = dt_utc.astimezone(tz) return local.strftime(f"%Y-%m-%d %H:%M ({tz_name} )" ) def schedule_meeting (): beijing_time = datetime(2025 , 9 , 30 , 14 , 0 , tzinfo=ZoneInfo("Asia/Shanghai" )) utc_time = beijing_time.astimezone(ZoneInfo("UTC" )) print ("存储用:" , utc_time.isoformat()) zones = [ "Asia/Shanghai" , "Asia/Tokyo" , "Europe/London" , "America/New_York" , "America/Los_Angeles" , ] for z in zones: print (format_for(utc_time, z)) if __name__ == "__main__" : schedule_meeting()
十五、常见陷阱
naive 与 aware 混用 :TypeError。生产系统统一 aware。
datetime.utcnow() 返回 naive :Python 3.12 已弃用它,用 datetime.now(tz=timezone.utc)。
.timestamp() 在 naive 上按本地时区解释 :跨时区时结果不对。
加一个月用 days=30 :不同月长度不同,用 relativedelta。
strptime 格式不匹配 :ValueError。加 try 或校验。
%f 精度 :Python 只到微秒(6 位),没有纳秒 datetime。要更精细用 time.time_ns。
夏令时(DST) :ZoneInfo 会自动处理,但边界时(”2 点重复”或”跳过 2 点”)依然有陷阱。
十六、小结与延伸阅读
四大类:date / time / datetime / timedelta;
存 UTC,展示时转当地时区;
用 zoneinfo(3.9+)或 pytz(3.8-);
ISO 8601 格式是万能通用格式;
加减用 timedelta,跨月用 relativedelta;
测代码性能用 time.perf_counter;
别混用 naive 和 aware;
datetime.utcnow() 已弃用。
延伸阅读:
下一篇 JSON 数据处理 我们讲怎么优雅地处理 JSON 数据。