首页
/ CPython 3.14 待移除特性清单:12 项标准库弃用移除的完整解读与迁移指南

CPython 3.14 待移除特性清单:12 项标准库弃用移除的完整解读与迁移指南

2026-09-06 13:59:03作者:钟日瑜

CPython 官方通过 Doc/deprecations/ 目录下的清单文件集中记录“将在某个版本中移除”的弃用特性,其中 pending-removal-in-3.14.rst 是 Python 3.14 的 Python 层移除清单,涵盖 argparseastasyncioitertoolsmultiprocessing 等 12 个模块的 API 变更。本文逐项解读这份清单中每个待移除特性的含义、替代方案,并结合当前仓库源码验证这些移除在实现层面的最终形态,帮助你在升级 Python 3.14 之前完成兼容性排查与代码迁移。

一、背景:这份“待移除清单”在 CPython 文档体系中的位置

CPython 的弃用(deprecation)遵循一个明确的流程:某个 API 先在文档中标记弃用、随后发出 DeprecationWarning,最后在目标版本中彻底移除。为了跟踪处于“等待移除”阶段的特性,维护者在 Doc/deprecations/ 目录下为每个大版本维护一份清单,例如 pending-removal-in-3.14.rstc-api-pending-removal-in-3.14.rst(C API 单独成册)以及 pending-removal-in-future.rst

需要注意适用前提:本清单描述的是 Python 3.14 的移除计划,条目大多是在 3.13 及更早版本中发出弃用警告、并承诺“3.14 移除”的特性。而当前仓库主分支的版本标识为 Include/patchlevel.h 中的 3.16.0a0,即仓库中的源码已经历了 3.14 的移除动作——这使得我们可以直接对照源码,验证“移除之后长什么样”,这是官方文档本身无法提供的纵深信息。

二、argparse:BooleanOptionalAction 的三个参数被移除

清单原文:

argparseBooleanOptionalActiontypechoicesmetavar 参数已弃用,将在 3.14 移除(gh-92248,Nikita Sobolev 贡献)。

BooleanOptionalAction 的作用是根据一个选项自动派生其否定形式(--foo 自动得到 --no-foo),它只接受布尔值,因此 type(值转换函数)、choices(取值集合)、metavar(帮助文本占位符)这三个参数在语义上毫无意义——它们从未被真正实现过。

从当前仓库源码可以看到移除的最终形态,Lib/argparse.pyBooleanOptionalAction.__init__ 的签名只剩下六个合法参数:

def __init__(self,
             option_strings,
             dest,
             default=None,
             required=False,
             help=None,
             deprecated=False):

其内部逻辑也印证了它只关心“正/负选项对”:对 --foo 形式拼接出 --no-foo,对 -foo 形式拼接出 -nofoo,并在命名冲突时直接抛 ValueError

迁移方式:检查代码中是否存在如下调用,删除多余的三个关键字参数即可:

# 移除前的写法(3.14 起无效)
parser.add_argument('--verbose',
                    action=argparse.BooleanOptionalAction,
                    type=bool,          # 删除
                    choices=[True, False],  # 删除
                    metavar='BOOL')       # 删除

# 迁移后的写法
parser.add_argument('--verbose',
                    action=argparse.BooleanOptionalAction,
                    default=False,
                    help='Enable verbose output')

三、ast:五个遗留节点类被移除,统一使用 Constant

清单原文:ast 模块的 ast.Numast.Strast.Bytesast.NameConstantast.Ellipsis 五个类自 3.8 起在文档中被弃用,3.14 起访问它们会发出 DeprecationWarning 并被移除,统一改用 ast.Constant(gh-90953,Serhiy Storchaka 贡献)。

这五个类是 Python 3.8 引入统一的 Constant 节点之前的历史遗留:当时每种字面量有自己的 AST 节点(Str 表示字符串、Num 表示数字、NameConstant 表示 True/False/NoneEllipsis 表示 ...Bytes 表示 b'...')。3.8 之后解析器只产生 ast.Constant,旧类仅保留为兼容别名。

在当前仓库的 Lib/ast.py 中可以直接确认移除结果:文件中已不存在 class Numclass Str 等定义(class Num 等类名在主 AST 定义文件中检索无结果)。

迁移方式:所有对 AST 节点的判断都应改为检查 Constant 及其 value 属性:

import ast

# 旧代码(针对 3.8 之前的解析结果)
if isinstance(node, ast.Str):
    print(node.s)

# 新代码(3.14 起的唯一正确写法)
if isinstance(node, ast.Constant) and isinstance(node.value, str):
    print(node.value)

注意区分:node.s / node.n 这类旧属性访问同样随旧节点类一起消失,必须使用 node.value。如果你维护的是 AST 转换工具(如 linter、代码生成器),建议用 ast.walk 遍历后用 isinstance(node, ast.Constant) 加类型判断替代所有历史分支。

四、asyncio:Child Watcher 体系整体移除

清单原文(gh-94597,Kumar Aditya 贡献)分两部分:

  1. asyncio.MultiLoopChildWatcherasyncio.FastChildWatcherasyncio.AbstractChildWatcherasyncio.SafeChildWatcher 四个子类观察者类已弃用,3.14 移除;
  2. asyncio.set_child_watcherasyncio.get_child_watcherasyncio.AbstractEventLoopPolicy.set_child_watcherasyncio.AbstractEventLoopPolicy.get_child_watcher 四个函数/方法一并移除。

Child Watcher 是 asyncio 监控子进程退出的旧机制(基于 SIGCHLD 信号或线程),它在多线程环境下容易与用户代码抢占信号处理,长期是 asyncio 的痛点。维护者内部早已用基于 pidfd(Linux 5.3+)和独立线程的私有实现替代了公开 API。

当前仓库 Lib/asyncio/unix_events.py 印证了移除后的状态:公开 API 中已无 ChildWatcher 家族,只剩两个私有类——_PidfdChildWatcherL891)与 _ThreadedChildWatcherL927),事件循环构造时按平台能力自动选择(优先 pidfd,不可用时回退线程方案)。从源码结构看,用户代码无需也不应再手动配置子进程监控器,它已成为 asyncio 的内部实现细节。

迁移方式:删除所有 asyncio.get_child_watcher() / asyncio.set_child_watcher(...) 调用及 SafeChildWatcher 等类的 isinstance 检查。对于“在多线程服务中安全运行子进程”这一原始诉求,直接用 loop.subprocess_exec() / loop.subprocess_shell() 即可,asyncio 内部会选择合适的 watcher。

get_event_loop 的弃用警告语义变化

清单同章节还有一条(gh-100160,Serhiy Storchaka 与 Guido van Rossum 贡献):默认事件循环策略的 asyncio.get_event_loop()没有当前事件循环且它决定要新建一个时,会发出 DeprecationWarning

这条不是“移除”,而是警告行为收紧:在协程内运行代码、或用 asyncio.run() 的场景不应手动调 get_event_loop()。迁移后的规范写法是:

# 异步代码内部:获取“正在运行”的循环
loop = asyncio.get_running_loop()

# 同步代码入口:直接托管给 asyncio.run
asyncio.run(main())

五、email:localtime 的 isdst 参数移除

清单原文:email.utils.localtimeisdst 参数已弃用(gh-72346,Alan Williams 贡献)。

该参数原本用于在“夏令时状态未知”的模糊时刻强制指定按夏/冬令时解释时间,但这一语义在 astimezone 转换路径中并不可靠。当前仓库 Lib/email/utils.py 中签名已简化为:

def localtime(dt=None):
    """Return local time as an aware datetime object.

    If called without arguments, return current time.  Otherwise *dt*
    argument should be a datetime instance, and it is converted to the
    local time zone according to the system time zone database.  If *dt*
    is naive (that is, dt.tzinfo is None), it is assumed to be in local time.
    """
    if dt is None:
        dt = datetime.datetime.now()
    return dt.astimezone()

迁移方式:调用时直接不传 isdst;需要精确时区语义时,改用带 zoneinfo.ZoneInfo 时区的 datetime 显式转换。

六、importlib.abc:资源读取协议类迁移到 importlib.resources.abc

清单原文(gh-93963,Jason R. Coombs 与 Hugo van Kemenade 贡献):importlib.abc.ResourceReaderimportlib.abc.Traversableimportlib.abc.TraversableResources 已弃用,改用 importlib.resources.abc.Traversableimportlib.resources.abc.TraversableResources

这轮迁移的背景是 PEP 645(资源 Traversable 协议)落地后,资源协议类被安置在语义更准确的 importlib.resources.abc 子模块中,importlib.abc 只保留与模块导入机制(PEP 302)直接相关的 ABC。

当前仓库中两侧的现状都可以验证:

  • 新位置 Lib/importlib/resources/abc.py 定义着 class Traversable(Protocol)L71)与 class TraversableResources(ResourceReader)L169);
  • 旧位置 Lib/importlib/abc.py__all__ 与类列表中已无这三个名字,仅保留 MetaPathFinderPathEntryFinderResourceLoaderL86)等导入机制 ABC。

迁移方式:修改 import 语句即可:

# 旧
from importlib.abc import Traversable, TraversableResources, ResourceReader

# 新
from importlib.resources.abc import Traversable, TraversableResources
# ResourceReader 若仍需鸭子类型检查,可参考 importlib.resources 提供的辅助函数

如果你的包通过 importlib.resources.files() 暴露资源,通常无需改动,只有做 isinstance 协议检查或自定义 ABC 实现时才受影响。

七、itertools:copy / deepcopy / pickle 支持被移除

清单原文(gh-101588,Raymond Hettinger 贡献):itertools 对 copy、deepcopy、pickle 的支持“没有文档记载、效率低下、历史上 bug 频发且行为不一致”,将在 3.14 移除,以显著减少代码量与维护负担。

这条移除的实际影响面比看上去大:此前 itertools 的迭代器 C 实现普遍实现了 tp_copytp_reduce 等槽,导致 copy.copy(it)pickle.dumps(it) 在部分迭代器类型上“碰巧能工作”,但语义不可靠。移除后统一行为是:迭代器对象不可复制、不可 pickle(抛出 TypeError)。

当前仓库 Modules/itertoolsmodule.c 反映了精简后的状态:全文件中 __copy__ 仅保留在一处——tee 迭代器的 tee_copyL1121,返回“独立迭代器”tee_copy_impl),其余迭代器类型不再携带复制/序列化槽。

迁移方式:如果代码依赖“pickle 一个进行到一半的迭代器”来断点续算,必须改为显式设计:

# 反模式:pickle 迭代器中间状态(3.14 起不可用)
data = itertools.cycle(range(100))
next(data)
state = pickle.dumps(data)   # TypeError

# 正确做法:自行维护可序列化状态
class ResumableCycler:
    def __init__(self, source):
        self.source = list(source)
        self.index = 0
    def __iter__(self):
        while True:
            yield self.source[self.index % len(self.source)]
            self.index += 1

依赖 copy.deepcopy 复制迭代器链(例如把 itertools.tee 之外的迭代器 deepcopy 给多个消费者)的代码,应改为在源头重新构造迭代器,或改用 tee 显式分叉。

八、multiprocessing:默认启动方式改为更安全的选择

清单原文(gh-84559):Linux、BSD 及其他非 macOS POSIX 平台(当前默认 'fork')的默认启动方式将改为更安全的方式;由于大多数代码不关心具体方式,运行时警告被认为过于扰人而不加。若你的代码必须使用 'fork',应通过 multiprocessing.get_contextmultiprocessing.set_start_method 显式指定。

fork 在多线程进程中不安全(子进程只继承发起 fork 的线程,锁状态、已分配内存都可能不一致),这正是变更动机。该条目值得特别强调的实操要点是:“需要 fork”从默认变成了必须声明

当前仓库 Lib/multiprocessing/context.py 中可以看到 3.14 起生效的最终实现:

    # bpo-33725: running arbitrary code after fork() is no longer reliable
    # on macOS since macOS 10.14 (Mojave). Use spawn by default instead.
    # gh-84559: We changed everyones default to a thread safeish one in 3.14.
    if (
        reduction.HAVE_SEND_HANDLE
        and sys.platform != 'darwin'
        # gh-155717: forkserver requires to write temporary files
        and util._has_writeable_tempdir()
    ):
        _default_context = DefaultContext(_concrete_contexts['forkserver'])
    else:
        _default_context = DefaultContext(_concrete_contexts['spawn'])

即:在支持 forkserver(需要 HAVE_SEND_HANDLE,即 sendfile 句柄传递)且临时目录可写的非 Darwin 平台,默认启动方式为 forkserver;否则回退到 spawnfork 仍然可用,只是不再默认。

迁移方式

import multiprocessing

# 显式声明“我就是要 fork”(两条等价路径)
ctx = multiprocessing.get_context('fork')
ctx.Process(target=worker, args=(payload,)).start()

# 或在入口一次性设定进程内默认方式
multiprocessing.set_start_method('fork', force=True)

排查方法:全局搜索直接 import multiprocessing 后使用 multiprocessing.Process / Pool / Queue 的代码路径——这些隐式走默认上下文;若进程内运行着线程池、I/O 多路复用或第三方库后台线程,切到 forkserver/spawn 后需确认 if __name__ == '__main__' 保护齐全、可被 pickle 的对象满足 spawn 的序列化要求。

九、pathlib:is_relative_to 与 relative_to 禁止多参数

清单原文:pathlib.PurePath.is_relative_toPurePath.relative_to 传入额外参数(多个 other)已弃用。

这两个方法自诞生起就接受可变的多个 *other 路径,但多参数用法(等价于逐个调用)从未被广泛依赖。当前仓库 Lib/pathlib/types.py 中两者的签名已收敛为单参数:

def relative_to(self, other, *, walk_up=False):   # L237
def is_relative_to(self, other):                  # L259

迁移方式:多个基准路径改为循环判断:

p = Path('/a/b/c/file.txt')

# 旧:一次传多个基准(3.14 起弃用/移除)
# p.is_relative_to(Path('/a'), Path('/x/y'))

# 新:逐个判断
any(p.is_relative_to(base) for base in (Path('/a'), Path('/x/y')))

十、pkgutil:find_loader 与 get_loader 发出 DeprecationWarning

清单原文(gh-97850,Nikita Sobolev 贡献):pkgutil.find_loaderpkgutil.get_loader 现在会发出 DeprecationWarning,应改用 importlib.util.find_spec

这两个函数是 PEP 302 早期设计的“绕过 import 直接找 loader”的旧通道,长期与导入机制的演化(path hooks、finders 链)脱节,行为在复杂导入场景下不可靠。当前仓库 Lib/pkgutil.py 中已无这两个公开函数(文件内仅在内部实现注释/调用处残留 find_loader 字样),移除已完成。

迁移方式

# 旧
import pkgutil
loader = pkgutil.get_loader('some.module')
found = pkgutil.find_loader('some.module') is not None

# 新
import importlib.util
spec = importlib.util.find_spec('some.module')
found = spec is not None

注意语义差异:find_spec 找不到模块时返回 None(或抛 ModuleNotFoundError,取决于调用方式),而旧 find_loader 失败时返回 None 但不抛异常——迁移时用 try/except ModuleNotFoundError 或先检查包是否存在来对齐行为。

十一、pty:master_open 与 slave_open 改用 openpty

清单原文:pty.master_open() 请用 pty.openpty 替代;pty.slave_open() 同样用 pty.openpty 替代。

master_open / slave_openopenpty 的“拆半”版本,各自只返回一端 fd,历史上还带有“master 端不设置 O_NONBLOCK”这类与 openpty 不一致的行为差异。当前仓库 Lib/pty.py 的公开函数列表中已无这两个函数(grep "^def" 仅剩 openptyforkspawnamaster/aslave 包装等),移除已完成。

迁移方式

import pty, os

# 旧
master, slave = pty.master_open(), pty.slave_open()
os.close(slave)

# 新(一次拿两端,master 端默认非阻塞)
master, slave = pty.openpty()

注意 openpty 返回的 master 端默认带 O_NONBLOCK 标志,若旧代码依赖阻塞式 master_open() 行为,需要手动 os.clear 该标志或在读取循环中处理 EAGAIN

十二、sqlite3:version/version_info 与命名占位符的序列参数

清单原文包含两条 sqlite3 变更:

  1. 模块级数据 sqlite3.versionsqlite3.version_info 移除;
  2. Cursor.execute / Cursor.executemany 在使用命名占位符:name)而 parameters 传的是序列(而非 dict)时,该用法弃用并将在 3.14 移除。

第 1 条的动机是消除与 C 层 sqlite3.version / Python 层包装版本混淆的双套版本常量。当前仓库 Lib/sqlite3/dbapi2.py 中只保留了 SQLite 库版本相关的 sqlite_version_info(由 sqlite_version 字符串解析而来),模块级 version / version_info 已不存在。迁移方式是改用 sqlite3.sqlite_version(SQLite 库版本);若需要的是 Python 本身版本,使用 sys.version

第 2 条针对的是“用元组喂给命名占位符”这种隐式按位置匹配的模糊用法,要求显式传字典:

# 旧(命名占位符 + 序列,3.14 起移除)
cur.execute("SELECT * FROM users WHERE id = :uid AND name = :name",
            (42, "alice"))

# 新(命名占位符必须配 dict)
cur.execute("SELECT * FROM users WHERE id = :uid AND name = :name",
            {"uid": 42, "name": "alice"})

顺带说明:若坚持用序列传参,请将 SQL 改写成位置占位符(?qmark 风格),那是与序列参数明确配套的形式。相关文档参见 Doc/library/sqlite3.rst 的 placeholder 一节。

十三、urllib.parse:Quoter 移除

清单原文(gh-88168,Gregory P. Smith 贡献):urllib.parse.Quoter 已弃用,因为它“本来就不是设计成公开 API”。

Quoter 是 URL 编码实现内部的字典缓存辅助类,早期版本直接暴露,后来一直作为半私有存在。当前仓库 Lib/urllib/parse.py 中该定义已更名为下划线前缀的 _Quoter,公开 API 中不再出现 Quoter;对外仍由 urllib.parse.quote / unquote 等函数承担编码职责(urlspliturlencode 等公开函数在模块中均不暴露 Quoter)。

迁移方式:如果你的代码直接构造了 Quoter 实例做字符串转义,请替换为对应的公开函数:

# 旧
from urllib.parse import Quoter
q = Quoter(safe='/:?[]@!$&\'()*+,;=')
encoded = q(value)

# 新
from urllib.parse import quote
encoded = quote(value, safe="/:?[]@!$&'()*+,;=")

十四、系统性排查:如何在代码库中找出所有受影响的调用

以上 12 个条目分属不同模块,建议在升级前做一轮自动化扫描,而不是依赖人工记忆:

  1. 静态扫描关键符号,对以下名字在代码库中全文检索(含字符串形式的反射调用): ast.Numast.Strast.Bytesast.NameConstantast.Ellipsisget_child_watcherset_child_watcherSafeChildWatcherFastChildWatcherMultiLoopChildWatcherimportlib.abc.ResourceReaderimportlib.abc.Traversablepkgutil.find_loaderpkgutil.get_loaderpty.master_openpty.slave_opensqlite3.versionsqlite3.version_infourllib.parse.Quoter

  2. 运行时暴露告警:用严格模式跑完整测试套件,把弃用警告变成错误:

    python -W error::DeprecationWarning -X dev -m unittest discover -s test
    

    -X dev 会额外开启开发模式检查(未触发警告的弃用行为、资源泄漏提示等),-W error::DeprecationWarning 让清单中“发出警告”类的条目(如 get_event_loop 建环警告、sqlite3 命名占位符误用)在第一次触发时即失败;

  3. 关注行为变更类条目itertools 的 copy/deepcopy/pickle 移除和 multiprocessing 默认启动方式变更不产生告警(文档中明确说明不加运行时警告),只能靠上述第 1 步的代码审查和回归测试覆盖。

十五、小结

Doc/deprecations/pending-removal-in-3.14.rst 所记录的 3.14 移除清单整体呈现三条主线:

  • API 收敛ast 五节点归一为 Constantimportlib.abc 资源协议迁入 importlib.resources.abcpty 两端 open 函数归一到 openptypkgutil loader 查询归一到 importlib.util.find_spec——旧入口被新入口完全取代,迁移通常只是改名与改 import;
  • 行为安全化multiprocessing 默认启动方式离开 forkasyncio.get_event_loop() 收紧警告——目标是把“碰巧能用”的默认行为变成显式声明;
  • 实现细节私有化itertools 的复制/序列化支持、urllib.parse.Quotersqlite3 模块级版本常量——从未承诺过稳定的内部实现被彻底收回。

结合本仓库源码可以确认,这些移除在 3.16 主分支中均已落地(如 Lib/argparse.py 的简化签名、Lib/multiprocessing/context.pyforkserver 默认分支、Lib/urllib/parse.py_Quoter 私有化)。对照本文各节的“迁移方式”逐项处理后,代码即可平滑运行于 Python 3.14 及之后的版本。

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