首页
/ CPython 3.20 弃用前瞻:即将移除的 5 项语言与标准库特性及迁移指南

CPython 3.20 弃用前瞻:即将移除的 5 项语言与标准库特性及迁移指南

2026-09-06 14:51:10作者:郜逊炳

本文基于 CPython 仓库中 Doc/deprecations/pending-removal-in-3.20.rst 官方弃用公告,系统讲解将在 Python 3.20 中移除的五类特性:struct.Struct 构造函数行为、24 个标准库模块的 __version__ 等版本属性、PEP 829 引入的 .pth 文件行为变化、抽象 AST 节点的直接实例化,以及非显式 runtime_checkable 协议类的 isinstance/issubclass 检查。读完后你将掌握每一项弃用的确切语义、仓库中的源码级实现证据,以及面向 3.20 的完整迁移方案。

弃用公告机制:为什么会有 pending-removal 文档

CPython 的弃用流程通常分为“弃用(deprecation,发出 DeprecationWarning)”和“移除(removal)”两个阶段。Doc/deprecations/ 目录按目标版本归档了每一批待移除的特性,pending-removal-in-3.20.rst 记录的就是“当前开发版已经弃用、将在 3.20 正式移除”的条目。这意味着:这些特性在 3.20 之前的版本中仍可用,但会发出弃用警告;升级路径应以该文件为清单,逐项排查代码

一、struct.Struct:禁止无 format 参数构造,禁止重复初始化

弃用内容

公告原文包含两条相互关联的弃用(由 Sergey B Kirpichev 和 Serhiy Storchaka 提交):

  1. 调用 struct.Struct__new__() 方法时不传 format 参数已被弃用,将在 3.20 移除;
  2. 对已初始化的 Struct 对象再次调用 __init__() 方法已被弃用,将在 3.20 移除。

源码实现证据

Struct 由 C 扩展实现,核心构造逻辑位于 Modules/_struct.c

  • s_new()(约 L1883-L1950)负责解析 format 参数。当未以“恰好一个位置参数”或“单个 format= 关键字参数”形式提供 format,且类没有自定义 __init__ 覆盖时,源码在 L1899-L1916 显式发出 DeprecationWarning
    • 参数多于一个时:"Struct() takes at most 1 argument (N given)"
    • 完全缺失时:"Struct() missing required argument 'format' (pos 1)"
  • 重复初始化的弃用位于 Struct___init___impl()(约 L1980-L2003):对象上维护了 s_formatinit_called 两个状态位。若对象已持有 format 且再次以不同格式初始化,会发出警告 "Re-initialization of Struct by calling the __init__() method will not work in future Python versions"(L1989-L1996);而 s_init()(L2005-L2019)则在“__init__ 被解释器隐式调用、但 __new__ 已完成全部工作”时直接短路返回,避免重复处理。

迁移方案

正确的构造方式始终是一次性传入 format 字符串,之后视其为不可变的编译产物:

import struct

# 推荐:构造时提供 format(唯一可靠方式)
s = struct.Struct(">I4d")
data = s.pack(1234, 1.5, 2.5, 3.5, 4.5)
values = s.unpack(data)

# 不要依赖以下两种即将移除的行为:
# 1. s2 = struct.Struct()            # 无 format 参数构造
# 2. s2 = struct.Struct.__new__(struct.Struct)  # 同上
# 3. 对已构造对象再次 s2.__init__("<h") 重新初始化

若确实需要“不同格式”,直接新建 Struct 实例即可——构造开销很低(本质上只是预解析 format 字符串并计算对齐后的 size/len,见 Modules/_struct.cset_format() 的解析流程),无需复用对象。

二、标准库模块级版本属性:__version__ / version / VERSION 全面移除

弃用内容

以下 24 个模块中的 __version__versionVERSION 属性被弃用,将在 3.20 移除,统一改用 sys.version_info(由 Hugo van Kemenade 和 Stan Ulbrych 提交):

模块 备注
argparse __version__ 返回硬编码值
csv
ctypes
ctypes.macholib 仅 macOS 可用
decimal 改用 decimal.SPEC_VERSION
http.server
imaplib
ipaddress
json
logging __date__ 一并弃用
optparse
pickle
platform
re
socketserver
tabnanny
tarfile
tkinter.font
tkinter.ttk
wsgiref.simple_server
xml.etree.ElementTree
xml.sax.expatreader
xml.sax.handler
zlib

源码实现证据

Lib/argparse.py 为例,模块顶层使用 PEP 562 的模块级 __getattr__ 实现惰性弃用(L2972-L2976):

def __getattr__(name):
    if name == "__version__":
        warnings._deprecated("__version__", remove=(3, 20))
        return "1.1"  # Do not change
    raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

注意 warnings._deprecated(..., remove=(3, 20)) 中的 remove=(3, 20) 参数——它生成的警告文案会明确提示“将在 3.20 移除”。Lib/json/init.py(L390-L393)采用完全相同的模式。这种实现还带来一个副作用值得注意:一旦 3.20 移除该属性,访问 argparse.__version__ 将抛出 AttributeError,因此依赖 getattr(module, "__version__", "unknown") 兜底取值的代码会静默降级,而直接属性访问的代码会直接崩溃。

迁移方案

import sys

# 替代 json.__version__ / zlib.VERSION 等所有模块级版本读取
major, minor = sys.version_info[:2]
if sys.version_info >= (3, 12):
    ...

两个例外需要特别处理:

  • decimal:公告明确指出应改用 decimal.SPEC_VERSION,因为它记录的是 Decimal 模块实现的规格版本,与解释器版本无对应关系:

    import decimal
    print(decimal.SPEC_VERSION)  # 如 (3, 0)
    
  • logging.__date__:一并弃用,如需日志库日期信息请直接读取源码版本控制信息,不要依赖运行时属性。

三、PEP 829:.pth 文件的 import 行与编码要求

弃用内容

这是 PEP 829 在 3.20 落地时的两项具体弃用(由 Barry Warsaw 提交):

  1. .pth 文件中的 import:在 {name}.pth 文件里写 import 语句是旧有的“副作用钩子”,此后每出现此类行都会产生弃用警告;
  2. .pth 文件编码:默认解码方式从 locale 编码改为强制 utf-8-sig(即允许带 BOM 的 UTF-8)。PEP 的措辞是“MUST be encoded in utf-8-sig”,即包含非 ASCII 字符的 .pth 文件若仍按 locale 编码保存,行为将不再可预期。

背景与迁移

site 模块在启动时扫描 site-packages 中的 .pth 文件:其中的目录行用于扩展 sys.path,而 import 行则会在解释器启动阶段执行任意代码——这正是它被弃用的原因(启动期代码执行难以追踪且影响启动行为)。

迁移建议:

  • 不要.pth 文件中写 import xxx。确需在导入包时执行初始化逻辑,应在包自身的 __init__.py 中完成;确需注册钩子,优先考虑在应用入口或 sitecustomize.py 中显式处理。
  • .pth 文件包含中文注释等非 ASCII 内容,请用 UTF-8(带 BOM 更稳妥)重新保存,并在部署后确认 site 能正常解析,避免在旧 locale 环境下行为不一致。

四、ast:禁止直接实例化抽象 AST 节点

弃用内容

公告指出:创建抽象 AST 节点实例(如 ast.ASTast.expr 等中间抽象类)已被弃用,将在 3.20 直接抛出错误(不再是警告)。

影响与迁移

ast 模块的类层次中,ast.ASTast.stmt / ast.expr / ast.keyword 等抽象中间层用于分类,而 ast.Moduleast.FunctionDefast.Constantast.Name 等具体节点才是可构造对象。3.20 之后:

import ast

# 3.20 之前:可实例化(当前开发版已弃用);3.20 起将抛错
node = ast.expr()        # 不要这样写
node = ast.AST()          # 不要这样写

# 正确做法:只构造具体节点,或通过解析/转换获得节点树
tree = ast.parse("x = 1")
tree = ast.Module(body=tree.body, type_ignores=[])   # 具体节点,合法

依赖“构造抽象节点作为占位/基类探针”的第三方库或 AST 生成器需要在 3.20 前改为构造具体节点(如用 ast.Constant 代替笼统的 ast.expr 占位)。

五、typing:隐式 runtime-checkable 协议的 isinstance 检查

弃用内容

公告由 Bartosz Sławecki 提交:对未显式用 @runtime_checkable 装饰、但继承自某个 runtime-checkable 协议类的协议类执行 isinstance / issubclass 检查已被弃用,3.20 起将抛出 TypeError

语义解析

typing 中,runtime_checkable 原本可以“隐式继承”——父协议被装饰后,子协议也被动获得运行时检查资格。但隐式资格带来两个问题:一是使用者无法从子类定义处看出它“碰巧可被 isinstance”;二是结构兼容性的运行时检查本身只能核查公有成员存在性,语义弱于显式声明。因此 3.20 的收紧方向是:想要运行时协议检查,就必须在类定义处显式装饰

from typing import Protocol, runtime_checkable

@runtime_checkable
class Reader(Protocol):
    def read(self, n: int = -1) -> bytes: ...

class BufferedReader(Reader):
    def read(self, n: int = -1) -> bytes: ...

# 3.20 之前:isinstance(b, Reader) 可用(Reader 隐式继承了 runtime_checkable)
# 3.20 起:将抛 TypeError,除非显式装饰

@runtime_checkable
class BufferedReader(Reader):
    def read(self, n: int = -1) -> bytes: ...

迁移策略:对每个需要 isinstance 的协议,显式添加 @runtime_checkable;对只想做静态类型约束的协议,保持不装饰并把运行期 isinstance 替换为显式的接口/类型标注。

小结:面向 3.20 的排查清单

# 弃用特性 3.20 后果 替代方案
1 Struct() 无 format / 对已初始化 Struct 调 __init__ 移除 构造时一次性传入 format,改格式就新建实例
2 24 个标准库模块的 __version__/version/VERSION 移除,访问抛 AttributeError sys.version_info;decimal 用 SPEC_VERSION
3 .pth 文件 import 行、非 utf-8-sig 编码 产生警告 / 解码行为改变 移除 import 行;.pth 一律 utf-8-sig 保存
4 抽象 AST 节点(ast.ASTast.expr)实例化 抛出错误 只构造具体节点或用 ast.parse
5 隐式 runtime-checkable 协议上的 isinstance/issubclass 抛出 TypeError 显式 @runtime_checkable 装饰

排查手段建议:在当前开发版开启 -X dev(默认显示弃用警告)运行完整测试套件,DeprecationWarning 的文案中带有 remove in 3.20 字样,可直接定位上述五类用法;涉及 C 实现的第 1 项可对照 Modules/_struct.cs_new()(L1883-L1950)与 Struct___init___impl()(L1980-L2003)的警告分支确认具体触发条件,涉及 Python 标准库的第 2 项则可搜索各模块顶层的 def __getattr__(name)warnings._deprecated 调用逐一核对。

登录后查看全文
热门项目推荐
相关项目推荐