Skip to content

版本选择策略 ​

Python 不像 Java、Node.js 那样提供官方「长期支持版(LTS)」,它的版本策略更接近滚动发布:每年 10 月推出一个特性版本(feature release),每个版本有固定的完整支持期与安全支持期。因此「该用哪个 Python 版本」需要结合发布节奏、依赖生态、部署环境三者一起判断。

前置知识

阅读本节前,建议先了解:uv 工具链管理

基础概念 ​

选择 Python 版本时,首先要区分三个容易混淆的概念:

概念含义示例
特性版本(feature release)每年新增语言特性的大版本,版本号形如 3.X.03.12.0、3.13.0
缺陷修复版本(bugfix release)同一特性版本下的修复更新,只修 bug 不加特性3.13.1、3.13.2
维护状态(maintenance status)官方对该版本提供何种级别的支持bugfix、security、end-of-life

需要强调的是:3.13 与 3.13.1 属于同一条产品线。升级 3.13.0 → 3.13.1 是打补丁,通常无需改代码;而 3.12 → 3.13 是跨特性版本升级,可能涉及废弃 API 的移除与行为变更。

选择版本时可参考的维度:

维度偏向新版本偏向旧版本
语言特性结构化模式匹配、类型参数语法等新特性老代码无需适配
性能解释器持续优化,新版本通常更快已实测过的稳定性能曲线
生态兼容主流库一般在新版发布后半年内跟进冷门库可能长期未适配新版本
安全支持支持窗口更长临近 EOL 时风险升高
部署环境云平台与基础镜像更新较快系统自带 Python 更新缓慢

一个实用的经验法则:生产环境选「上一个特性版本的最新补丁版」,即新版本发布后先观望一个周期,等主流依赖完成适配再跟进。这个策略在保守与激进之间取得平衡。

语法与用法 ​

Python 版本会在多个地方被声明,保持这些声明一致是避免「本地能跑、线上报错」的关键。

在 pyproject.toml 声明项目支持的 Python 范围(PEP 621):

toml
[project]
name = "my-service"
version = "0.1.0"
# 声明本包运行所需的 Python 版本范围,供安装器与依赖解析器校验
requires-python = ">=3.11"

在版本管理器中声明具体解释器:

bash
# pyenv:写入 .python-version
pyenv local 3.13.1

# asdf:写入 .tool-versions
asdf set python 3.13.1

# uv:写入 .python-version 并管理解释器
uv python pin 3.13

在容器镜像中固定解释器:

bash
# Dockerfile 中固定到具体小版本,避免构建漂移
FROM python:3.13.1-slim

在 CI 中覆盖多个版本:

yaml
# .github/workflows/ci.yml(节选)
strategy:
  matrix:
    python-version: ["3.11", "3.12", "3.13"]

版本说明符遵循 PEP 440,常见写法与含义如下:

写法含义建议场景
>=3.113.11 及以上都可运行库项目,范围尽量宽
>=3.11,<3.143.11 到 3.13已知 3.14 尚未适配
==3.13.*锁定 3.13 系列应用项目,锁定特性版本
~=3.11.0等价于 >=3.11.0,<3.12.0谨慎使用,限制过窄

需要注意:requires-python 只约束「安装时」的版本检查,运行时若解释器版本不符才会报错;而版本管理器写入的 .python-version 只影响本地目录,不会随包分发。两者职责不同,不能互相替代。

详细说明 ​

三种典型的版本策略 ​

不同团队对风险的容忍度不同,可以归纳为三种策略:

策略做法优点代价
保守型始终落后最新稳定版 1 个特性版本生态成熟、坑最少无法使用新特性,安全窗口更短
稳健型跟随最新稳定版的最新补丁特性与稳定兼顾需要持续关注依赖适配
激进型主版本一发布即切换最早获得性能与新特性依赖未适配时需自行绕过

多数团队适合稳健型。判断依据很简单:只要项目依赖的核心库(如 Web 框架、ORM、科学计算库)已声明支持目标版本,就可以跟进。

生产环境与开发环境的差异 ​

开发环境与生产环境对版本的要求侧重点不同:

  • 开发环境:可以使用较新版本,便于提前发现兼容性问题;允许多版本共存,方便做矩阵测试。
  • 生产环境:优先使用经过验证的版本,并要求与开发、预发布环境严格一致。
  • CI 环境:通常覆盖项目声明的全部 requires-python 范围,作为兼容性守门员。

推荐做法是让「开发 → CI → 预发布 → 生产」使用同一特性版本,仅补丁号允许不同;补丁号差异也必须记录在锁定文件中,避免不可复现。

依赖生态的滞后效应 ​

Python 生态庞大,新版本发布后各类库的适配速度差异明显:

  • 纯 Python 库通常几天到几周即可声明支持。
  • 含 C 扩展的库(numpy、pandas、cryptography 等)需要重新编译 wheel,往往滞后数周到数月。
  • 依赖已废弃模块的库,可能因 PEP 594 等移除而直接不可用。

因此升级前应先用工具检查依赖声明,而不是等运行时报错。下面的脚本可以帮你快速筛查。

python
# check_requires_python.py
from __future__ import annotations

from importlib.metadata import distributions


def collect_requires_python() -> dict[str, str]:
    """汇总当前环境中各分发包声明的 Python 版本要求。"""
    result: dict[str, str] = {}
    for dist in distributions():
        name = dist.metadata["Name"]
        # 元数据中的 Requires-Python 字段即 pyproject.toml 的 requires-python
        requires = dist.metadata.get("Requires-Python")
        if name and requires:
            result[name] = requires
    return result


if __name__ == "__main__":
    for package, spec in sorted(collect_requires_python().items()):
        print(f"{package:30} {spec}")

requires-python 的边界含义 ​

requires-python 是「可运行范围」,不是「已测试范围」。写 >=3.11 意味着安装器允许在 3.11 以上任意版本安装,但不代表作者真的测过 3.14。库作者常见的做法是:

  1. 声明一个较宽的范围,如 >=3.11;
  2. 在 CI 中跑多版本矩阵;
  3. 当某个新版本出现不兼容时,临时收紧上界或修复问题。

应用项目则相反:应当收紧范围(如 ==3.13.*),因为应用只需跑在自己控制的环境里,锁定越严格越安全。

容器基础镜像的选择 ​

选择 Python 官方镜像时可参考以下命名规律:

标签形式说明适用
python:3.13.1完整 Debian 版,体积较大需要编译工具链
python:3.13.1-slim精简版,无编译工具多数生产服务
python:3.13.1-alpine基于 Alpine,体积最小静态依赖、体积敏感
python:3.13浮动到 3.13 系列最新补丁不想频繁改标签

注意 alpine 使用 musl libc,某些预编译 wheel 不兼容,会在安装依赖时回退到源码编译。若依赖里有 numpy 这类科学计算库,优先选 slim。

版本升级的三种路线 ​

从旧版本迁移到新版本时,可选择:

  • 一次性升级:直接改版本号并修复全部报错。适合小项目或版本跨度小的情况。
  • 渐进式升级:逐个特性版本升级(3.9 → 3.10 → 3.11 …)。每次改动小,问题定位快。
  • 并行运行:新旧版本同时存在于 CI,逐步把测试矩阵的默认值切到新版本。

跨度较大(跨 3 个以上特性版本)时,建议采用渐进式,并借助 python -X dev 与 -W error::DeprecationWarning 提前暴露废弃用法。

本教程的版本基准 ​

本教程以 Python 3.12 及以上 为基准。原因有三:

  1. 3.12 引入了 PEP 695 的类型参数语法(class Foo[T]:)与 PEP 701 的 f-string 语法放宽,这些在示例中经常出现;
  2. 3.13 起新增的交互式解释器(见 Python 3.13 改进的 REPL)是 1.6 节的核心内容;
  3. 3.12 与 3.13 目前都处于官方支持期内,示例可直接运行。

当示例涉及某个版本才有的语法时,文中会显式标注所需版本。若你的环境低于基准,可先用版本管理器安装 3.12 或 3.13 再跟随练习。

版本号背后的语义:Python 的实践 ​

Python 的版本号并不完全遵循「语义化版本(SemVer)」,而是遵循 PEP 440 定义的规则。关键差异在于:

号段SemVer 含义Python 的实际含义
主版本不兼容变更3 代表 Python 3 这一代,长期不变
次版本向后兼容的新功能特性版本,可能包含不兼容变更
修订号向后兼容的修复缺陷修复与安全修复

也就是说,Python 的「次版本」升级并不保证向后兼容——废弃 API 的移除、标准库模块的删除都可能发生。因此不能按 SemVer 的直觉去理解 3.12 → 3.13。

预发布版本的写法同样有规范:3.14.0a1、3.14.0b2、3.14.0rc1 分别表示 alpha、beta、release candidate。安装器默认不会选中预发布版,除非显式声明,例如 >=3.14.0b1。

何时必须升级:触发条件清单 ​

并非所有项目都需要主动追新,但以下情况出现时,升级就从「可选」变成「必须」:

  • 当前版本进入 end-of-life,不再获得安全补丁;
  • 依赖的核心库在新版本中停止支持你的 Python 版本;
  • 云平台或托管服务下架了旧版本运行时;
  • 系统基础镜像(如 Debian、Ubuntu)更新后不再自带旧版本;
  • 目标库的新特性只在更高版本 Python 上可用。

反之,以下情况可以暂缓升级:项目已进入维护期、无新需求;升级成本远大于收益;依赖链中存在长期未适配的库。

版本兼容范围的写法建议 ​

项目类型推荐写法理由
命令行工具(发布到 PyPI)>=3.10用户环境多样,范围宜宽
Web 服务(自部署)==3.12.*环境可控,锁定更安全
内部库>=3.11,<3.14限定已验证范围,留出余量
一次性脚本不声明直接用当前解释器即可

写范围时避免两个极端:一是过宽(如 >=3.6)却在低版本上根本无法运行;二是过窄(如 ==3.12.7)导致用户装不上。前者是虚假声明,后者是过度约束。

实战示例 ​

示例一:为新项目确定目标版本 ​

假设要启动一个 Web 服务,依赖 FastAPI、SQLAlchemy、uvicorn。决策步骤如下:

bash
# 1. 看看官方当前有哪些仍在支持期的版本
#    可访问 https://devguide.python.org/versions/ 查看
#    假设当前 3.13 为最新稳定版,3.12 为上一版本

# 2. 检查核心依赖是否已声明支持 3.13
uv pip install --dry-run fastapi sqlalchemy uvicorn 2>&1 | head -n 20

# 3. 用 uv 安装并固定版本
uv python install 3.13
uv python pin 3.13

# 4. 验证
uv run python -c "import sys; print(sys.version)"

若依赖已适配,则选择 3.13;若有任一核心依赖未适配,则退回到 3.12 并等待其适配。不要为了用新语法而绕开未适配的依赖,那会给后期维护埋下隐患。

示例二:批量检查依赖是否兼容目标版本 ​

下面脚本使用标准库读取已安装分发包的 Requires-Python,并判断目标版本是否落在其声明的范围内。

python
# check_target_compat.py
from __future__ import annotations

import sys
from importlib.metadata import distributions

from packaging.specifiers import InvalidSpecifier, SpecifierSet
from packaging.version import Version


def target_version() -> str:
    """从命令行读取目标版本,缺省使用当前解释器版本。"""
    if len(sys.argv) > 1:
        return sys.argv[1]
    info = sys.version_info
    return f"{info.major}.{info.minor}.{info.micro}"


def check(target: str) -> list[tuple[str, str, bool]]:
    """返回 (包名, 版本范围, 是否兼容) 的列表。"""
    rows: list[tuple[str, str, bool]] = []
    for dist in distributions():
        name = dist.metadata["Name"]
        spec = dist.metadata.get("Requires-Python")
        if not name or not spec:
            continue
        try:
            # 判断目标版本是否满足该包声明的范围
            ok = Version(target) in SpecifierSet(spec)
        except (InvalidSpecifier, ValueError):
            # 少数包声明不规范,跳过而非中断
            continue
        rows.append((name, spec, ok))
    return rows


if __name__ == "__main__":
    target = target_version()
    print(f"检查目标版本:{target}\n")
    incompatible = 0
    for package, spec, ok in sorted(check(target)):
        mark = "OK  " if ok else "WARN"
        if not ok:
            incompatible += 1
        print(f"[{mark}] {package:28} requires-python {spec}")
    print(f"\n不兼容依赖数量:{incompatible}")

脚本用 packaging 解析版本说明符——它是打包生态的官方实现,通常随 pip 一同安装。若环境中没有,可先 uv pip install packaging。

示例三:渐进式升级一条业务线 ​

一个运行在 3.9 上的老服务要升到 3.12,跨了 3 个特性版本,采用渐进式路线:

bash
# 阶段一:3.9 -> 3.10
pyenv install 3.10.15
pyenv local 3.10.15
python -W error::DeprecationWarning -m pytest -q   # 把废弃警告当错误,提前暴露问题
# 修复全部告警与失败用例后,提交并部署到预发布

# 阶段二:3.10 -> 3.11
pyenv install 3.11.10
pyenv local 3.11.10
python -W error::DeprecationWarning -m pytest -q
# 3.11 移除了部分模块,需检查依赖是否仍可用

# 阶段三:3.11 -> 3.12
pyenv install 3.12.7
pyenv local 3.12.7
python -W error::DeprecationWarning -m pytest -q

# 最后把 requires-python 与容器镜像一并更新

每跨一个版本就完整跑一遍测试并部署到预发布,可以确保出问题时责任范围明确。跨度越大,越不能指望一次到位。

示例四:为依赖库确定兼容范围 ​

一个内部工具库要发布到公司的私有索引,需要确定 requires-python。做法是先跑一遍多版本测试,再据实测结果声明范围。

bash
# 用 uv 准备三个解释器
uv python install 3.11 3.12 3.13

# 依次在三个版本下跑测试
for ver in 3.11 3.12 3.13; do
  uv venv --python "$ver" ".venv-$ver"
  uv pip install --python ".venv-$ver/bin/python" -e . pytest
  echo "== Python $ver =="
  ".venv-$ver/bin/python" -m pytest -q || echo "该版本不兼容"
done

若三个版本全部通过,就在 pyproject.toml 写 requires-python = ">=3.11";若只有 3.12 以上通过,就写 >=3.12。声明的下限必须与实测的下限一致,不要凭猜测写宽范围。

注意事项 ​

  • 不要在生产使用 3.X.0:特性版本的首个发布往往带有较多回归问题,等两三个补丁版再上更稳妥。
  • 不要只声明主版本:写 python:3 会让镜像随上游浮动,某天可能突然切到不兼容的新版本;应固定到 3.13.1 或至少 3.13。
  • requires-python 与虚拟环境要一致:声明 >=3.11 却在 3.9 上开发,会在 CI 上集中爆发问题。
  • 注意 free-threaded 与 JIT 的成熟度:Python 3.13 起提供的自由线程构建与实验性 JIT 仍属实验特性,生产慎用。
  • EOL 版本不会收到安全补丁:一旦进入 end-of-life,即使发现高危漏洞也不会有官方修复。
  • 浮动标签不等于稳定:3.13 标签会在每个补丁发布后改变内容,严格可复现的构建应使用完整版本号或摘要。
  • 不要跨版本忽略废弃警告:DeprecationWarning 是官方给出的升级预告,忽略它等于把问题推迟到下一次硬性移除。
  • 不要同时维护过多版本:一个项目通常只需支持「当前稳定 + 上一个」两个特性版本,维护面越宽,测试与修复成本越高。
  • 不要忽略操作系统的自带 Python:macOS 与部分 Linux 发行版自带的 python3 往往偏旧,且被系统工具依赖,不要直接替换它,应使用版本管理器另装。
  • 不要用补丁版本差异解释线上问题:若开发与生产仅补丁号不同却行为不一致,优先怀疑锁文件未同步或镜像标签浮动。
  • 慎用预发布版本:a/b/rc 版本仅用于提前验证,不应进入任何长期运行的环境。

常见问题 ​

Q:为什么官方没有 LTS? Python 采用固定的年度发布节奏(见 Python 版本生命周期),每个特性版本都提供 5 年支持窗口,相当于「人人都有 LTS」,因此不需要再单独划分 LTS 版。

Q:可以用最新的特性版本吗? 可以,但要分清场景。个人项目、实验性代码可以直接用;生产服务建议等 2~3 个补丁版,观察是否有回归问题。

Q:3.13 和 3.13.1 在锁文件里有什么区别?3.13 是浮动指针,会随补丁更新而变化;3.13.1 是精确值,可复现。锁文件应记录精确值,而 pyproject.toml 可写范围。

Q:老项目一直在 3.8 上跑,有必要升级吗? 如果 3.8 已 EOL 且项目面向公网,就有必要。升级前先用 -W error::DeprecationWarning 跑测试,评估工作量,再按渐进式路线推进。

Q:free-threaded 构建(3.13t)可以用吗? 不建议在生产使用。它是实验特性,不少 C 扩展尚未适配,性能在单线程下还会有回退。可以在独立环境提前尝鲜,但不要作为默认解释器。

Q:同一个项目能同时支持 3.11 和 3.14 吗? 可以,但需要在 CI 中为两个版本各跑一次测试,并在代码里避免使用高版本独有的语法(或用条件分支兼容)。跨度越大,维护成本越高。

最佳实践 ​

  1. 应用项目锁定到特性版本(==3.13.*),库项目声明较宽范围(>=3.11)。
  2. 生产环境使用「上一个特性版本的最新补丁」或「最新特性版本的第二/三个补丁」。
  3. 让开发、CI、预发布、生产共用同一特性版本,差异只允许在补丁号且需锁文件记录。
  4. 升级前用脚本或 --dry-run 检查依赖的 Requires-Python,不要凭感觉升级。
  5. 采用渐进式升级,每跨一个特性版本就完整测试并部署到预发布。
  6. 在 CI 中开启 -W error::DeprecationWarning,把废弃警告当作必须处理的问题。
  7. 定期查看官方版本状态表(devguide),提前规划临近 EOL 的版本迁移。

下一节

继续学习:Python 版本生命周期

参考链接 ​