本节目标:弄明白 Python 2 到 3 为什么要断代、今天该选哪个 Python 版本,以及「3.12+」这个基线是怎么来的。
适用版本:Python 3.12+(实测 3.14.6)
1.2 Python 2 到 3、发布节奏与版本选择
上一节我们说「显式优于隐式」是 Python 的底层哲学。这条哲学在 2008 年引发了一场持续十二年的地震——Python 3.0 为了修正历史遗留的「隐式」设计,不惜与 Python 2 断代。这一节我们把这段历史和今天的版本现状讲清楚。
1.2.1 2008 年:为什么必须断一次
Python 3.0 于 2008 年 12 月发布。它不是一次普通的升级,而是一次故意不向后兼容的重写。原因很简单:Python 2 里积累了太多「隐式」行为,越晚修越贵。
Guido 的团队做了一个在当时看起来近乎疯狂的决定——同一个语言维护两条线:Python 2 继续修 bug、加功能,Python 3 慢慢重建生态。他们原以为迁移只要几年,结果拖了十二年。
1.2.2 三处具体的断裂
Python 3 改的东西很多,但对日常代码影响最大的是下面三处。它们共同的主题都是「消灭隐式」。
第一处:print 从语句变成函数。 Python 2 里 print 是关键字,写法是:
print "hello" # Python 2 写法
这行代码在 Python 3 里直接是语法错误:
SyntaxError: Missing parentheses in call to 'print'. Did you mean print(...)?
改成函数后,print("hello") 才统一了——它变成了普通函数,可以被传参、被替换、被 file=... 定向输出。代价是所有 Python 2 代码都得改。
第二处:整数除法。 Python 2 里两个整数相除,结果还是整数(向下取整),这是 C 语言的传统,但极易出错:
print(7 / 2) # Python 3 的结果
print(7 // 2) # 想要整除,必须显式写 //
真实输出:
3.5
3
在 Python 2 里 7 / 2 会得到 3。这个改动让 type(7 / 2) 从 int 变成了 float——显式的 // 表示整除,/ 一律表示真除法,这正是「显式优于隐式」的落地。
第三处:str 与 bytes 分离,文本默认 Unicode。 这是最深远的一处。Python 3 里 str 是「文本」,bytes 是「字节」,两者不能隐式混用:
s = "café"
b = s.encode("utf-8")
print("str len:", len(s), "bytes len:", len(b))
print("bytes:", b)
真实输出:
str len: 4 bytes len: 5
bytes: b'caf\xc3\xa9'
café 作为文本是 4 个字符,编码成 UTF-8 后是 5 个字节(é 占两个字节)。Python 2 的 str 其实是「字节串」,导致中文、emoji 处理起来处处是坑。Python 3 把这件事显式化了:你要处理文本就用 str,要处理字节就用 bytes,编码解码必须显式调用 .encode() / .decode()。
1.2.3 2008 到 2020:漫长的迁移
为了让迁移可行,Python 生态提供了几种机制:
| 工具 / 机制 | 作用 |
|---|---|
from __future__ import ... | 在 Python 2 里提前启用部分 Python 3 行为 |
2to3 工具 | 自动把 Python 2 代码转换成 Python 3 代码 |
six 库 | 写一份代码同时兼容 2 和 3 |
python-future | 更完整的兼容层 |
例如在 Python 2 里,你可以这样提前体验真除法:
from __future__ import division
Python 2.7 于 2010 年发布,是 2.x 的最后一个版本。官方原定 2015 年停止支持,一再延后,最终在 2020 年 1 月 1 日正式 EOL(End of Life)。此后连安全补丁都不再有。如果你今天还在维护 Python 2 代码,唯一正确的方向是迁移——这段历史的完整叙事可以看专题文章 Python 发展史:众人的语言(第一卷) 。
1.2.4 PEP 602:年度发布节奏
早期 Python 的发布没有固定节奏,3.0 到 3.1 隔了大半年,3.1 到 3.2 又隔了一年多。开发者没法规划「什么时候能用到新特性」。
PEP 602 确立了新的节奏,从 Python 3.9 开始生效:
- 每年 10 月发布一个 feature release(3.10、3.11、3.12……依此类推)。
- 每个版本提供约 5 年支持:先是 bugfix 阶段,之后进入只修安全问题的阶段。
这条规则对你有两个直接影响:一是你可以预期「每年十月有个新版本」,二是你可以放心用一个版本好几年,不必频繁升级。
1.2.5 3.12、3.13、3.14 的实际差异
光看版本号没意义,关键是每个版本给了什么。下表是本书主控在 Python 3.14.6 上逐条实测确认的结果:
| 版本 | 已实测可用的能力 |
|---|---|
| 3.11 | ExceptionGroup / except*、asyncio.TaskGroup、asyncio.timeout、tomllib |
| 3.12 | PEP 695 泛型语法(type X = ...、class C[T]:、def f[T]())、itertools.batched、pathlib.Path.walk、typing.override、sys.monitoring |
| 3.13 | 自由线程构建(实验性,PEP 703)、实验性 JIT、改进的交互式 REPL、typing.TypeIs、copy.replace、warnings.deprecated、os.process_cpu_count、dbm.sqlite3、PEP 594 移除「死电池」 |
| 3.14 | PEP 649/749 注解延迟求值(annotationlib)、PEP 734 多解释器(concurrent.interpreters)、PEP 750 模板字符串 t"..."、PEP 758 except 免括号、PEP 765 finally 中的控制流降级为警告、PEP 784 标准库 compression.zstd |
其中 3.12 的 PEP 695 泛型语法是本书第 9 章的重点——它让类型参数从 TypeVar 的繁琐写法收敛成 class Box[T]: 这种「明显的方式」。而 3.14 的 t"..." 模板字符串、except 免括号,都可以在本机直接验证:
# 3.14:except 可以省略括号(PEP 758)
try:
raise ValueError("demo")
except ValueError, TypeError:
print("PEP 758 except-no-parens: OK")
# 3.14:模板字符串(PEP 750)
name = "Ada"
t = t"hello {name}"
print("t-string type:", type(t).__name__)
真实输出:
PEP 758 except-no-parens: OK
t-string type: Template
注意 t"..." 得到的不是 str,而是一个 Template 对象——这正是「显式优于隐式」的延续:模板把「插值」和「最终字符串」分成了两步,方便在拼接前做转义或校验。
1.2.6 3.15:还在预览,别当既成事实
写这一节时,Python 3.15 尚处于 3.15.0rc3(release candidate,候选发布版)阶段,还没有正式发布。所以本书提到 3.15 时,一律说「计划 / 预览」,不会把它当成已经可用的稳定版本。
如果你在别的文章里看到「3.15 已经支持某某特性」,先去看那篇文章的日期。RC 阶段的东西随时可能变,只有正式版(3.15.0)发布后,相关行为才算定下来。
1.2.7 如何确认你手上的版本
版本建议再好,也得先知道你手上跑的是哪一个。最直接的方式:
python3 --version
Python 3.14.6
在代码里则可以查 sys.version_info,它是一个具名元组,可以直接和版本号比较:
import sys
print(sys.version_info)
if sys.version_info >= (3, 12):
print("可以使用 PEP 695 泛型语法")
真实输出(3.14.6):
sys.version_info(major=3, minor=14, micro=6, releaselevel='final', serial=0)
可以使用 PEP 695 泛型语法
注意最后那个 releaselevel='final'——正式版是 final,候选版是 candidate。做特性检测时,优先用 sys.version_info 而不是解析版本字符串,前者是结构化的,不会因为字符串格式变化而失效。
1.2.8 近年版本发布时间线
把 PEP 602 生效后的版本排成表,「每年十月一个版本」的节奏就一目了然:
| 版本 | 首次发布 | 状态(本文写作时) |
|---|---|---|
| 3.10 | 2021-10 | 已进入安全修复期 |
| 3.11 | 2022-10 | 已进入安全修复期 |
| 3.12 | 2023-10 | 仍在支持期内,本书基线 |
| 3.13 | 2024-10 | 仍在支持期内 |
| 3.14 | 2025-10 | 当前稳定线 |
| 3.15 | 未发布 | 3.15.0rc3(预览) |
表中每个正式版本的首次发布都在十月,这正是 PEP 602 规定的节奏。具体日期以 Python 官方下载页 为准——记住这个页面,它是判断「某版本是否已发布」的唯一权威来源。
1.2.9 版本选择建议
结合上面的节奏与特性,本书给出如下建议:
| 场景 | 建议版本 | 理由 |
|---|---|---|
| 新项目 | 3.12 及以上 | 3.12 是本书基线,能用上 PEP 695 等现代语法 |
| 生产环境 | 当前稳定线 3.14 | 稳定、有完整支持期、生态适配充分 |
| 老项目 | 至少升到仍在支持期内的版本 | Python 2 与已 EOL 的 3.x 没有安全补丁 |
| 尝鲜 | 3.15 的 RC 版本 | 只能用于试验,不要放进生产 |
在项目里,这个选择会写进 pyproject.toml:
[project]
name = "myapp"
requires-python = ">=3.12"
requires-python = ">=3.12" 就是本书的基线声明。它不承诺「只能在 3.12 上跑」,而是告诉安装工具:「低于 3.12 的环境请拒绝安装。」这样你就能放心使用 3.12 引入的语法,而不用为 3.9、3.10 的兼容性写降级代码。
1.2.10 一张图记住这段历史
把关键节点串起来:
- 1991 0.9.0 发布,Python 起步。
- 2000 2.0 发布,奠定 2.x 时代。
- 2008 3.0 发布,与 2.x 断代。
- 2010 2.7 发布,2.x 收尾。
- 2019 PEP 602 确立年度节奏。
- 2020-01-01 Python 2 正式 EOL。
- 今天 稳定线是 3.14,3.15 在 RC 阶段。
理解了这段历史,你就不会再问「为什么教程里的代码在我这儿跑不通」——十有八九,那是 Python 2 时代的写法。
小结
- Python 3.0 于 2008 年发布,是一次故意不向后兼容的重写,核心是消灭历史遗留的隐式行为。
- 三处关键断裂是
print变函数、整数除法改真除法、str与bytes分离并默认 Unicode。 - 迁移拖了十二年,Python 2 于 2020 年 1 月 1 日正式 EOL。
- PEP 602 确立每年 10 月发布、每个版本约 5 年支持的节奏。
- 新项目用 3.12+,生产用当前稳定线 3.14,3.15 尚在 3.15.0rc3,不得当成既成事实。
下一节我们从历史转向内部:Python 说自己是「解释型语言」,但它到底是怎么把一行源码变成执行结果的?搞懂这一点,你才能理解为什么它既不算纯解释、也不算纯编译。
阅读导航:上一节:1.1 Python 的诞生与设计哲学 · 下一节:1.3 解释器实现与执行模型 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。