<p>Python 3.11 中 pytest 协程异常捕获行为变化源于解释器对异常链和 ExceptionGroup 语义更严格,非 pytest 自身修改;--pdb 仅响应未捕获异常,无法进入 except* 块,需手动 breakpoint() 或禁用 xdist 并行。</p>

pytest 在 Python 3.11 中对协程异常捕获行为变化,根本原因不是 pytest 自身改了逻辑,而是 Python 解释器层面对异常链(__cause__/__context__)和 ExceptionGroup 的语义更严格了 —— pytest 只是如实反映这一底层变化。
Python 3.11 强化了异常链的传播规则
在 Python 3.10 及以前,嵌套异常(比如 raise ValueError() from KeyError())的传播路径较宽松,pytest 往往在最外层 assert 失败处中断;而 3.11 默认启用 faulthandler,且要求异常链必须显式、不可跳过地传递。这导致:
- 当协程中抛出
ExceptionGroup,再被except*捕获后,原始异常链可能被“截断”,pytest 认为它已被处理,不再触发--pdb - 协程内
await抛出的异常若经由asyncio.TaskGroup或asyncio.gather(..., return_exceptions=True)包裹,会变成ExceptionGroup成员,而非顶层未捕获异常 -
sys.last_value不再保留已捕获异常,你在breakpoint()里看到的可能是上一个测试残留的异常对象
pytest-xdist 并行模式下 --pdb 完全不可用
如果你用了 pytest -n auto 或 -n2,--pdb 会在子进程中启动,但终端无法与之交互 —— 这不是 bug,是设计限制:
- 子进程的 stdin/stdout 未连接到当前终端,
pdb启动后直接卡住 - 必须加
-n0禁用并行,才能让--pdb正常工作 - 若必须用 xdist,改用
logging.debug()+pytest --log-cli-level=DEBUG替代交互式调试
async def fixture 中 TaskGroup 异常不被捕获?
这不是 pytest 的问题,而是 fixture 生命周期和事件循环管理错位导致的静默降级:
立即学习“Python免费学习笔记(深入)”;
- 如果
@pytest.fixture没声明为async def,或没在pytest.ini中启用asyncio_mode = strict,pytest 会把async deffixture 当作同步函数执行 ——TaskGroup根本不会启动,自然也不会抛异常 - 即使 fixture 正确声明为异步,若依赖的
event_loopfixture 返回的是已关闭的 loop,TaskGroup构造时会直接 raiseRuntimeError: no running event loop,但该异常可能被 pytest 的 fixture 清理逻辑吞掉 - 验证方式:在 fixture 内第一行加
print(asyncio.get_running_loop()),确认 loop 确实正在运行
ExceptionGroup 失败时 pdb 停不进 except* 块
因为 --pdb 只响应**未捕获异常**,而 except* 是显式捕获,异常不会逃逸到测试函数顶层:
- 不要指望
--pdb自动停进except*块里 - 必须手动插入断点:
breakpoint()(推荐)或import pdb; pdb.set_trace() - 若真想让
--pdb触发,可在except*末尾加raise,但注意这会改变原逻辑,仅限临时调试
真正容易被忽略的点是:你看到的“异常位置”未必是你写的那行 assert,而是 ExceptionGroup 被聚合或 TaskGroup 调度失败的底层位置 —— 调试时得顺着 eg.exceptions 逐个 inspect,而不是只看 traceback 最上面一行。


















