datetime
datetime 是 Python 标准库中处理日期和时间的核心模块。它可以表示“2026 年 9 月 13 日”“10:30:00”“两小时后”等信息,也能完成日期计算、文本解析、格式化、时间戳转换和时区处理。
本文示例以 Python 3.10 及以上版本为基础。学完后,你应该能够:
- 根据需求选择
date、time、datetime和timedelta; - 创建、读取、比较和计算日期时间;
- 在日期时间对象、字符串和 Unix 时间戳之间转换;
- 区分不带时区与带时区的日期时间;
- 使用
timezone和zoneinfo正确处理 UTC、固定偏移与地区时区; - 避免夏令时、月份运算和
replace()误用等常见问题。
认识主要类型
通常直接从模块中导入需要的类型:
from datetime import date, datetime, time, timedelta, timezone
| 类型 | 表示的内容 | 示例 |
|---|---|---|
date | 日期,不包含一天中的具体时间 | 2026-09-13 |
time | 一天中的时间,可包含时区信息 | 14:30:00 |
datetime | 日期与时间的组合,可包含时区信息 | 2026-09-13 14:30:00+08:00 |
timedelta | 两个时间点之间的时长 | 2 天 3 小时 |
timezone | UTC 或固定偏移时区 | UTC+08:00 |
tzinfo | 时区信息的抽象基类 | 通常不直接实例化 |
这些对象都是不可变对象。对它们执行运算或调用 replace() 时,会返回新对象,不会修改原对象。
处理日期:date
创建日期
可以传入年、月、日直接创建:
from datetime import date
birthday = date(2000, 5, 20)
print(birthday) # 2000-05-20
print(birthday.year) # 2000
print(birthday.month) # 5
print(birthday.day) # 20
参数必须构成真实日期,否则会抛出 ValueError:
from datetime import date
try:
invalid_date = date(2025, 2, 29)
except ValueError as error:
print(error) # day is out of range for month
获取本地系统当前日期可以使用 today():
from datetime import date
today = date.today()
print(today)
星期、序号与 ISO 周日期
from datetime import date
day = date(2026, 9, 13)
print(day.weekday()) # 6:星期一为 0,星期日为 6
print(day.isoweekday()) # 7:星期一为 1,星期日为 7
print(day.timetuple().tm_yday) # 256:当年的第几天
print(day.isocalendar()) # datetime.IsoCalendarDate(year=2026, week=37, weekday=7)
ISO 周日期中的“周所属年份”可能与公历年份不同。例如,一月初的几天可能属于上一年的最后一个 ISO 周。需要按“第几周”统计时,应同时使用 isocalendar() 返回的 year 和 week。
也可以根据 ISO 年、周、星期反向创建日期:
from datetime import date
day = date.fromisocalendar(2026, 37, 7)
print(day) # 2026-09-13
处理一天中的时间:time
time 只表示时、分、秒和微秒,本身不包含具体日期:
from datetime import time
meeting_time = time(hour=14, minute=30, second=15, microsecond=500_000)
print(meeting_time) # 14:30:15.500000
print(meeting_time.hour) # 14
print(meeting_time.minute) # 30
print(meeting_time.second) # 15
print(meeting_time.microsecond) # 500000
datetime 的最小精度是微秒,即百万分之一秒。它不保存纳秒。
由于 time 没有日期,通常不能直接用它回答“两个时刻相差多久”。跨越午夜、夏令时切换等情况都需要日期上下文,应先组合成 datetime 再计算。
处理日期和时间:datetime
创建日期时间
from datetime import datetime
published_at = datetime(2026, 9, 13, 14, 30, 15, 500_000)
print(published_at) # 2026-09-13 14:30:15.500000
print(published_at.date()) # 2026-09-13
print(published_at.time()) # 14:30:15.500000
未提供的时间参数默认为 0:
from datetime import datetime
start_of_day = datetime(2026, 9, 13)
print(start_of_day) # 2026-09-13 00:00:00
可以把 date 和 time 组合成 datetime:
from datetime import date, datetime, time
day = date(2026, 9, 13)
clock = time(14, 30)
result = datetime.combine(day, clock)
print(result) # 2026-09-13 14:30:00
获取当前时间
datetime.now() 默认返回系统本地时间,但不携带时区信息:
from datetime import datetime
local_now = datetime.now()
print(local_now)
需要一个明确的 UTC 时间点时,应创建带时区的对象:
from datetime import datetime, timezone
utc_now = datetime.now(timezone.utc)
print(utc_now) # 例如 2026-09-13 06:30:00+00:00
datetime.utcnow() 返回的对象没有时区信息,容易被误当成本地时间。它从 Python 3.12 起已弃用,新代码应使用 datetime.now(timezone.utc)。
读取与替换字段
datetime 提供年、月、日、时、分、秒和微秒等只读属性:
from datetime import datetime
moment = datetime(2026, 9, 13, 14, 30, 15)
print(moment.year) # 2026
print(moment.month) # 9
print(moment.day) # 13
print(moment.hour) # 14
print(moment.minute) # 30
print(moment.second) # 15
replace() 可以复制对象并替换指定字段:
from datetime import datetime
original = datetime(2026, 9, 13, 14, 30)
start_of_hour = original.replace(minute=0, second=0, microsecond=0)
print(original) # 2026-09-13 14:30:00
print(start_of_hour) # 2026-09-13 14:00:00
replace() 是“替换字段”,不是“加减时间”。例如把 2026-01-31 的月份直接替换为 2 会得到无效日期并抛出 ValueError。
表示时间差:timedelta
timedelta 表示一段持续时间,可以由周、天、小时、分钟、秒、毫秒和微秒构造:
from datetime import timedelta
duration = timedelta(days=2, hours=3, minutes=30)
print(duration) # 2 days, 3:30:00
print(duration.days) # 2
print(duration.seconds) # 12600,仅是不足一天的秒数
print(duration.total_seconds()) # 185400.0,完整时长的总秒数
构造参数会被规范化,内部只保存 days、seconds 和 microseconds:
from datetime import timedelta
duration = timedelta(hours=27)
print(duration.days) # 1
print(duration.seconds) # 10800,即剩余 3 小时
需要完整秒数时使用 total_seconds(),不要只读取 seconds。
日期时间加减
from datetime import date, datetime, timedelta
today = date(2026, 9, 13)
print(today + timedelta(days=7)) # 2026-09-20
start = datetime(2026, 9, 13, 9, 0)
end = start + timedelta(hours=2, minutes=30)
print(end) # 2026-09-13 11:30:00
print(end - start) # 2:30:00
date - date 和 datetime - datetime 会得到 timedelta。date + timedelta 和 datetime + timedelta 则得到新的日期或日期时间。
比较与排序
同类日期时间对象可以直接比较:
from datetime import datetime
deadline = datetime(2026, 9, 13, 18, 0)
submitted_at = datetime(2026, 9, 13, 17, 45)
if submitted_at <= deadline:
print("按时提交")
它们也可以作为排序键:
events = [
datetime(2026, 9, 14, 9, 0),
datetime(2026, 9, 13, 18, 0),
]
events.sort()
print(events)
字符串格式化与解析
日期时间经常需要在“对象”和“文本”之间转换:
- 格式化:日期时间对象变为字符串;
- 解析:字符串变为日期时间对象。
优先使用 ISO 8601
ISO 8601 格式清晰、容易排序,也适合系统之间交换数据:
from datetime import date, datetime, time, timezone
day = date(2026, 9, 13)
clock = time(14, 30, 15)
moment = datetime(2026, 9, 13, 14, 30, 15, tzinfo=timezone.utc)
print(day.isoformat()) # 2026-09-13
print(clock.isoformat()) # 14:30:15
print(moment.isoformat()) # 2026-09-13T14:30:15+00:00
对应的 fromisoformat() 可以解析兼容的 ISO 格式:
from datetime import date, datetime, time
day = date.fromisoformat("2026-09-13")
clock = time.fromisoformat("14:30:15")
moment = datetime.fromisoformat("2026-09-13T14:30:15+08:00")
print(day)
print(clock)
print(moment)
Python 3.11 及以上版本的 datetime.fromisoformat() 可以直接解析末尾的 Z,它表示 UTC。兼容 Python 3.10 时,可先把 Z 替换为 +00:00:
from datetime import datetime
text = "2026-09-13T14:30:15Z"
moment = datetime.fromisoformat(text.replace("Z", "+00:00"))
自定义输出格式:strftime()
from datetime import datetime
moment = datetime(2026, 9, 13, 14, 5, 9)
print(moment.strftime("%Y-%m-%d %H:%M:%S")) # 2026-09-13 14:05:09
print(moment.strftime("%Y年%m月%d日")) # 2026年09月13日
按格式解析:strptime()
from datetime import datetime
text = "2026/09/13 14:05"
moment = datetime.strptime(text, "%Y/%m/%d %H:%M")
print(moment) # 2026-09-13 14:05:00
格式必须和文本匹配,否则会抛出 ValueError。处理用户输入时应捕获错误并给出清晰提示:
from datetime import datetime
text = "2026-02-30"
try:
day = datetime.strptime(text, "%Y-%m-%d").date()
except ValueError:
print("请输入 YYYY-MM-DD 格式的有效日期")
常用格式符
| 格式符 | 含义 | 示例 |
|---|---|---|
%Y | 四位年份 | 2026 |
%m | 两位月份 | 09 |
%d | 两位日期 | 13 |
%H | 24 小时制小时 | 14 |
%I | 12 小时制小时 | 02 |
%M | 分钟 | 05 |
%S | 秒 | 09 |
%f | 六位微秒 | 123456 |
%z | UTC 偏移 | +0800 |
%a | 本地化的星期简称 | 取决于系统语言环境 |
%A | 本地化的星期全称 | 取决于系统语言环境 |
%j | 一年中的第几天 | 256 |
%W | 以星期一为每周首日的周序号 | 36 |
星期和月份名称会受到操作系统语言环境影响,不适合依赖固定中文输出。跨系统交换数据时优先使用 isoformat();面向用户展示时,再由应用的国际化层处理。
Unix 时间戳
Unix 时间戳表示从 1970-01-01 00:00:00 UTC 到某一时刻经过的秒数。它表示一个绝对时间点,不包含原始时区。
日期时间转换为时间戳
from datetime import datetime, timezone
moment = datetime(2026, 9, 13, 14, 30, tzinfo=timezone.utc)
timestamp = moment.timestamp()
print(timestamp)
对不带时区的 datetime 调用 timestamp() 时,Python 会把它解释为系统本地时间。同一段代码在不同时区运行可能得到不同结果,因此转换时间戳前应明确时区。
时间戳转换为日期时间
from datetime import datetime, timezone
timestamp = 1_789_309_800
utc_moment = datetime.fromtimestamp(timestamp, tz=timezone.utc)
local_moment = datetime.fromtimestamp(timestamp)
print(utc_moment) # 带 UTC 时区信息
print(local_moment) # 系统本地时间,不带时区信息
需要明确、可移植的结果时,应向 fromtimestamp() 传入 tz。
不同系统可表示的时间戳范围可能不同,特别久远或遥远的日期可能抛出 OverflowError 或 OSError。
理解无时区与带时区对象
Python 文档常把日期时间分为两类:
- 无时区(naive):
tzinfo is None,对象本身不能说明它属于哪个时区; - 带时区(aware):包含能够确定 UTC 偏移的
tzinfo,可以表示时间线上的明确时刻。
from datetime import datetime, timezone
naive = datetime(2026, 9, 13, 14, 30)
aware = datetime(2026, 9, 13, 14, 30, tzinfo=timezone.utc)
print(naive.tzinfo) # None
print(aware.tzinfo) # UTC
两者用途不同:
- “商店每天 09:00 开门”是墙上时钟规则,可以先用无时区时间表示;
- “订单在全球统一时间的哪一刻创建”应使用带时区的
datetime; - 单纯的生日或账单日期通常使用
date,不应强行转换成午夜时间点。
不能对无时区和带时区的 datetime 进行顺序比较或相减,否则会抛出 TypeError。业务中应统一约定时间模型,而不是在计算发生时临时猜测时区。
使用 UTC 与固定偏移时区
timezone.utc 表示 UTC:
from datetime import datetime, timezone
moment = datetime(2026, 9, 13, 6, 30, tzinfo=timezone.utc)
print(moment.isoformat()) # 2026-09-13T06:30:00+00:00
timezone 也可以表示固定偏移:
from datetime import datetime, timedelta, timezone
utc_plus_8 = timezone(timedelta(hours=8))
moment = datetime(2026, 9, 13, 14, 30, tzinfo=utc_plus_8)
print(moment.isoformat()) # 2026-09-13T14:30:00+08:00
固定偏移只表示“始终比 UTC 快或慢多少”,不知道地区的夏令时和历史规则。处理 Asia/Shanghai、Europe/Paris 等真实地区时,应使用标准库 zoneinfo。
使用地区时区:zoneinfo
zoneinfo 从 Python 3.9 起进入标准库,它读取 IANA 时区数据库:
from datetime import datetime
from zoneinfo import ZoneInfo
shanghai = ZoneInfo("Asia/Shanghai")
new_york = ZoneInfo("America/New_York")
meeting = datetime(2026, 9, 13, 20, 0, tzinfo=shanghai)
new_york_time = meeting.astimezone(new_york)
print(meeting.isoformat())
print(new_york_time.isoformat())
系统需要提供 IANA 时区数据。在部分 Windows 或精简环境中可能缺少这些数据,此时可以安装由 Python 维护的 tzdata 包。它不是本项目文章示例运行所必需的依赖,应根据实际部署环境决定是否安装。
正确转换时区
astimezone() 保持绝对时间点不变,只改变显示所用的时区:
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
utc_moment = datetime(2026, 9, 13, 6, 30, tzinfo=timezone.utc)
shanghai_moment = utc_moment.astimezone(ZoneInfo("Asia/Shanghai"))
print(utc_moment) # 2026-09-13 06:30:00+00:00
print(shanghai_moment) # 2026-09-13 14:30:00+08:00
print(utc_moment.timestamp() == shanghai_moment.timestamp()) # True
不要用 replace(tzinfo=...) 转换时区:
# 这只是更换时区标签,不会调整时、分、秒
wrong = utc_moment.replace(tzinfo=ZoneInfo("Asia/Shanghai"))
只有当一个无时区对象的各字段本来就代表某地墙上时间,并且你确定该时区时,才可以用 replace(tzinfo=...) 为它附加时区。转换已经带时区的时间应使用 astimezone()。
夏令时与 fold
采用夏令时的地区,某些本地时间可能:
- 在时钟向前拨时根本不存在;
- 在时钟向后拨时出现两次。
datetime 的 fold 属性用于区分重复时间:fold=0 表示较早的一次,fold=1 表示较晚的一次。
from datetime import datetime
from zoneinfo import ZoneInfo
new_york = ZoneInfo("America/New_York")
first = datetime(2026, 11, 1, 1, 30, tzinfo=new_york, fold=0)
second = datetime(2026, 11, 1, 1, 30, tzinfo=new_york, fold=1)
print(first.utcoffset()) # -1 day, 20:00:00,即 UTC-04:00
print(second.utcoffset()) # -1 day, 19:00:00,即 UTC-05:00
标准库不会在构造对象时自动拒绝所有不存在或有歧义的本地时间。接收用户预约时间时,应明确地区时区,并根据业务要求检查夏令时歧义;高可靠系统通常把确定后的时间点转换为 UTC 存储,同时保留原地区时区名称用于展示和规则追踪。
日历日期不等于固定时长
timedelta 适合“经过 90 分钟”“七天后”等固定时长,但不支持月和年,因为每月、每年的天数并不固定:
from datetime import date, timedelta
day = date(2026, 1, 31)
print(day + timedelta(days=30)) # 2026-03-02,并不是“下个月同一天”
处理“下个月最后一天”“一年后的续费日”等日历规则时,需要先定义业务语义。可以使用 calendar.monthrange() 自己实现简单规则:
import calendar
from datetime import date
def add_one_month(day: date) -> date:
if day.month == 12:
year, month = day.year + 1, 1
else:
year, month = day.year, day.month + 1
last_day = calendar.monthrange(year, month)[1]
return date(year, month, min(day.day, last_day))
print(add_one_month(date(2026, 1, 31))) # 2026-02-28
print(add_one_month(date(2024, 1, 31))) # 2024-02-29
复杂的按月、按工作日或节假日计算通常应使用经过验证的专用库,并先写清楚月底、闰年和夏令时规则。
常见实用场景
计算年龄
年龄不能简单地用天数除以 365,因为存在闰年且生日是否已到会影响结果:
from datetime import date
def calculate_age(birthday: date, today: date) -> int:
if birthday > today:
raise ValueError("生日不能晚于计算日期")
birthday_has_passed = (today.month, today.day) >= (
birthday.month,
birthday.day,
)
return today.year - birthday.year - (not birthday_has_passed)
print(calculate_age(date(2000, 10, 1), date(2026, 9, 13))) # 25
对于 2 月 29 日出生者,还需根据业务或法律规则明确非闰年的生日认定日期。
计算倒计时
from datetime import datetime, timezone
deadline = datetime(2026, 12, 31, 16, 0, tzinfo=timezone.utc)
now = datetime.now(timezone.utc)
remaining = deadline - now
if remaining.total_seconds() <= 0:
print("已截止")
else:
print(f"还剩 {remaining.days} 天")
若倒计时用于控制超时,系统时钟可能因校时而跳变。短时程序计时更适合使用 time.monotonic();datetime 更适合表示日历上的截止时间。
按日期分组
from collections import defaultdict
from datetime import datetime
events = [
datetime(2026, 9, 13, 9, 0),
datetime(2026, 9, 13, 15, 0),
datetime(2026, 9, 14, 10, 0),
]
grouped = defaultdict(list)
for event in events:
grouped[event.date()].append(event)
for day, day_events in sorted(grouped.items()):
print(day, len(day_events))
如果事件来自不同时区,应先转换到业务指定的地区时区,再调用 date()。直接按 UTC 日期分组,可能把当地凌晨的事件分到前一天。
常见错误与最佳实践
混用无时区和带时区对象
from datetime import datetime, timezone
naive = datetime.now()
aware = datetime.now(timezone.utc)
# aware - naive # TypeError
明确项目约定:表示真实时间点时优先使用带时区对象,内部通常统一为 UTC,在界面和报表边界转换成用户地区时区。
把 replace(tzinfo=...) 当成时区转换
replace() 只改字段或标签;astimezone() 才会保持同一时刻并换算显示时间。
使用 timedelta.seconds 读取总秒数
seconds 只包含规范化后不足一天的部分。读取完整时长应调用 total_seconds()。
用固定天数表示一个月或一年
月份长度、闰年和业务规则都不固定。涉及月、季度、年时,应采用明确的日历算法。
依赖模糊的日期文本
01/02/2026 可能表示 1 月 2 日,也可能表示 2 月 1 日。数据交换应使用带年份的 ISO 8601 格式;用户输入应显示格式提示并严格校验。
把日期时间当作持续计时器
性能统计和超时测量应使用 time.perf_counter() 或 time.monotonic(),避免系统时间调整影响结果。
选择指南
| 需求 | 推荐做法 |
|---|---|
| 生日、账单日、节假日 | date |
| 每天固定的开门时间 | time,并在应用层保留地区时区规则 |
| 订单创建、日志记录等明确时间点 | 带时区的 datetime,通常以 UTC 存储 |
| 两小时、七天等固定时长 | timedelta |
| 跨系统交换日期时间 | ISO 8601,并携带 Z 或 UTC 偏移 |
| 真实地区的时区和夏令时 | zoneinfo.ZoneInfo |
| 月份、季度、工作日等日历运算 | 明确业务规则并使用日历算法或专用库 |
| 性能耗时与超时 | time.perf_counter() 或 time.monotonic() |
小结
date表示日期,time表示一天中的时间,datetime同时表示日期和时间;timedelta表示固定时长,不能直接表达“一个月”或“一年”;- 对外交换数据优先使用 ISO 8601,复杂自定义格式再使用
strftime()和strptime(); - 表示真实时间点时,应使用带时区的
datetime,并明确 UTC 与地区时区的转换边界; astimezone()用于时区转换,replace(tzinfo=...)只替换时区标签;- 真实地区时区应使用
zoneinfo,并认真处理夏令时造成的重复或不存在时间; - 时间戳转换、日期分组、月度计算都应先明确时区和业务语义。