跳到主要内容

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 小时
timezoneUTC 或固定偏移时区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
%H24 小时制小时14
%I12 小时制小时02
%M分钟05
%S秒09
%f六位微秒123456
%zUTC 偏移+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,并认真处理夏令时造成的重复或不存在时间;
  • 时间戳转换、日期分组、月度计算都应先明确时区和业务语义。