编程 DeprecationWarning 默认不显示:五行默认过滤器、-W 的模块名匹配与 pytest 优先级反转

2026-10-21 00:04:59

DeprecationWarning 默认不显示:过滤器链路、pytest 语义差异与 CI 里的 error 配置

现象:单文件脚本能看到,跑测试/跑服务看不到

把同一段触发弃用 API 的代码分别放进 python foo.py 和 pytest,结果常常相反:脚本在 stderr 上打印一行 DeprecationWarning,测试却一句不吭。原因是 Python 3.7 起(PEP 565)默认过滤器列表的第一条是 default::DeprecationWarning:__main__,只对 __main__ 里直接触发的警告生效。单文件脚本的模块名恰好是 __main__;而 zipapp 的 __main__.py、可执行包的 __main__ 子模块、console_scripts 生成的入口包装脚本,模块名都不是 __main__,因此照样被下面的 ignore::DeprecationWarning 吞掉。

默认过滤器只有五行

常规 release 构建的列表,按优先级:

default::DeprecationWarning:__main__
ignore::DeprecationWarning
ignore::PendingDeprecationWarning
ignore::ImportWarning
ignore::ResourceWarning

default 表示每个位置(module + lineno)只打印第一次。3.7 之前没有第一条,DeprecationWarning 从 3.2 起就整体被忽略,副作用是开发者基本看不到自己发的弃用警告,PEP 565 只补了这一条。FutureWarning 面向应用最终用户,默认显示。debug 构建的默认过滤器列表为空——这点在后面会咬人。

七个 action 与四元组语义

过滤器条目是 (action, message, category, module, lineno),按顺序匹配,第一条命中的决定处置方式,全不命中走 default。action 有七个:default(每个位置一次)、error(抛异常)、ignore、always(all 是其别名)、module(每个模块一次,忽略行号)、once(全局一次)。

匹配语义在两个入口上不一样,这是最容易踩的:

  • message 是正则,必须匹配警告消息的开头,大小写不敏感。在 warnings.filterwarnings() 里永远按正则处理;在 -W / PYTHONWARNINGS 里,只有写成 /.../ 才是正则,否则是字面字符串前缀(忽略首尾空白)。
  • module 是正则,匹配完全限定模块名的开头,大小写敏感。在 -W / PYTHONWARNINGS 里,只有 /.../ 才是正则,否则必须与完全限定模块名完全相等。所以 -W ignore::DeprecationWarning:jsonschema 匹配不到 jsonschema.validators,要写 -W "ignore::DeprecationWarning:/jsonschema/"。

插入顺序:filterwarnings 前插,catch_warnings 不重置

warnings.filterwarnings(...) 默认插到列表最前面(append=True 才追加到末尾),靠前的条目优先于靠后的。simplefilter 同理,只是不涉及正则。

catch_warnings() 的语义是保存并恢复当前 filter 列表,不是重置成默认列表。所以下面这种写法在 -Werror 下没用:

with warnings.catch_warnings():
    warnings.simplefilter("ignore")   # 只是临时覆盖
    do_something()                    # 里面仍可能抛 DeprecationWarning

如果错误是在进入 with 之前就被 error 条目抛出的,或者警告来自被其他机制(例如 warnings.warn 的 stacklevel 归到别的模块)触发的代码,catch_warnings 也拦不住。可靠做法是按模块名精确 ignore,而不是临时塞一个 context manager。

-W 与 PYTHONWARNINGS

-W 只作用于当前进程,追加到 sys.warnoptions;PYTHONWARNINGS 通过环境变量生效,会被子进程继承。两者共同点是:只要其中之一设置了,运行时就不再套用默认过滤器。文档给测试跑者的推荐写法正是利用这一点,同时把值塞回环境变量以影响子进程:

import sys
if not sys.warnoptions:
    import os, warnings
    warnings.simplefilter("default")
    os.environ["PYTHONWARNINGS"] = "default"

pytest 的两套语义

pytest 默认会显示用户代码和第三方库的 DeprecationWarning / PendingDeprecationWarning。但一旦测试里用了 pytest.warns()、pytest.deprecated_call() 或 recwarn fixture 捕获警告,输出就完全不显示;一旦解释器层已有 PYTHONWARNINGS 或 -W,pytest 也不再自行配置任何默认过滤器。

filterwarnings ini 选项的含义与 -W 接近但解析不同:ini 里的 message 是正则(匹配开头、大小写不敏感),-W 里默认是字面前缀。更要紧的是优先级方向相反:一个警告命中多个选项时,执行最后匹配的那个选项(因为每条都前插,后应用的最终排最前),而 warnings.filterwarnings() 是「最先匹配者生效」。

@pytest.mark.filterwarnings 优先于命令行和 ini。多个装饰器叠加时,装饰器逆序求值,先写的反而优先;单个装饰器传多个参数,后传的优先;用 pytestmark 赋列表则又回到「后者优先」。

还有一条容易忽略:pytest 3.8(提交 9965ed8)起,只有当 -W、filterwarnings ini、sys.warnoptions 三者都没配置时,pytest 才注入 always::DeprecationWarning 和 always::PendingDeprecationWarning。也就是说,你只要写了一条 filterwarnings,pytest 就不再帮你显示 DeprecationWarning 了——想保留显示,得显式加 always::DeprecationWarning。

CI 实操:三步收紧

**第一步,先观测,不阻断。**把警告数量降下来再谈 error:

PYTHONWARNINGS=default::DeprecationWarning python -m pytest -q

PYTHONWARNINGS 会继承给子进程,覆盖测试发现、导入阶段这些普通 runner 管不到的地方。想知道是哪条依赖在发,挂一个 showwarning:

import sys, traceback, warnings

def _show(message, category, filename, lineno, file=None, line=None):
    if issubclass(category, DeprecationWarning):
        sys.stderr.write(f"{filename}:{lineno}: {category.__name__}: {message}\n")
        traceback.print_stack(limit=6)

warnings.showwarning = _show
warnings.simplefilter("always", DeprecationWarning)

放在 conftest.py 顶层或 sitecustomize.py 里,跑一遍全量测试,按 traceback 最上层 warnings.warn 调用帧的模块名归类。注意警告归属取决于 stacklevel,有时会被算到 builtins 或其他模块头上,按错误信息猜模块名经常猜错。

**第二步,逐条白名单收窄。**清掉自己代码里的警告后,对暂时修不了的第三方依赖按模块名放行:

[tool.pytest.ini_options]
filterwarnings = [
    "error",
    "ignore::DeprecationWarning:jsonschema.*",
]

这里 module 走 filterwarnings() 的正则语义,jsonschema.* 能匹配子模块名。

**第三步,全量启用。**白名单收敛到只剩确实不可修依赖后,把 error 提升为全类别,或在命令行加兜底:

python -W error::DeprecationWarning -m pytest

命令行 -W 的 module 是精确匹配,写空即匹配全部模块;需要按前缀时用 /regex/ 形式。若同时用了 ini 和 -W,注意 ini 通常后应用、优先级更高。

什么时候别急着升 error

  • **警告来自不可修的第三方或 vendored 代码。**cpython 自己曾长期无法用 -Werror 跑测试,原因就是 pip 里 vendored 模块发的弃用警告。这类情况只能按模块名 ignore,不能指望上游立刻修。
  • **SyntaxWarning 在编译期发出,运行时的过滤器可能抑制不住。**把它放进 error 会以不合预期的方式中断导入。
  • **debug 构建的默认过滤器列表为空。**CI 镜像如果用的是 debug 版解释器,default::DeprecationWarning:__main__ 和几条 ignore 都不存在,行为与 release 版不同。
  • **测试发现/导入阶段与子进程。**unittest 默认 runner 只在测试执行期间覆盖过滤器,发现阶段和子进程里的 DeprecationWarning 仍看不到;要覆盖这些路径,用 PYTHONWARNINGS 而不是 -W。

参考资料

推荐文章

程序员茄子在线接单