<p>ExceptionGroup 和 except 是为多异常并发失败提供可读、可分、可追溯处理的新语义基础设施,需配合 asyncio.gather 等批量执行使用,传统 except 无法捕获,必须用 except 或显式处理 ExceptionGroup 实例。</p>

它不简化“单异常”处理逻辑,而是让原本无法表达的“多异常并发失败”场景有了可读、可分、可追溯的处理路径。如果你的代码没用到 asyncio.gather、asyncio.TaskGroup 或 concurrent.futures 批量执行,那 ExceptionGroup 和 except* 对你几乎无感——不是语法糖,是新语义的基础设施。
asyncio.gather 抛出 ExceptionGroup 时,传统 except 完全失效
默认 return_exceptions=False 下,只要任一协程出错,gather 就抛出 ExceptionGroup,而不是你写的那个具体异常。
- 写
except ValueError:→ 永远进不去,因为外层是ExceptionGroup实例,不是ValueError - 写
except Exception:→ 也进不去,因为ExceptionGroup继承自BaseException,不继承Exception - 写
except BaseException:→ 能捕获,但会吞掉KeyboardInterrupt和SystemExit,破坏程序可控退出 - 正确做法:必须用
except* ValueError:直接匹配子异常,或先except ExceptionGroup:再手动遍历eg.exceptions
except* 不是“多个 except 的简写”,而是子异常筛选器
except* 块之间互不干扰,每个子异常独立尝试匹配,匹配成功就归入对应的新 ExceptionGroup,不会跳过后续块(除非是子类关系且已进入更具体的块)。
- 一个
ValueError和一个TypeError同时存在时:except* ValueError和except* TypeError都会触发 - 如果写了
except* Exception在最后,它只会拿到没被前面更具体类型捕获的剩余子异常 -
except* OSError会同时捕获OSError及其子类(如FileNotFoundError),和普通except行为一致 - 不能在
except*块里用raise直接重新抛出原ExceptionGroup;想传播未处理的子异常,得显式raise eg
日志和调试时,str(eg) 是个陷阱
print(eg) 或 str(eg) 只显示摘要,比如 "ExceptionGroup: batch failed (2 sub-exceptions)",看不到任何 traceback,也无法定位哪条协程、哪行代码出错。
立即学习“Python免费学习笔记(深入)”;
- 要完整输出所有子异常的 traceback,必须用
traceback.print_exception(eg) - 若需结构化记录(例如发到 Sentry),应遍历
eg.exceptions,对每个exc单独调用traceback.format_exception(type(exc), exc, exc.__traceback__) -
eg.subgroup(TypeError)可返回仅含TypeError子项的新ExceptionGroup或None,适合做条件分流,但注意它不递归进入嵌套的ExceptionGroup
pytest 测试 ExceptionGroup 必须升级且写法不同
pytest ExceptionGroup,pytest.raises(ValueError) 在它内部时永远失败。
- 确认版本:
pip show pytest→ Version ≥ 7.4.0 - 测试显式构造的组:
with pytest.raises(ExceptionGroup, match="msg"):,不能写pytest.raises(ValueError) - 验证子异常构成:在
with块内检查exc_info.value.exceptions的长度和类型 - 匹配子异常内容(7.4.2+):
match="msg^ValueError:bad",注意这是匹配整个repr()字符串,不是纯消息文本 - 协程测试必须加
@pytest.mark.asyncio或显式await,否则raises不进事件循环
真正容易被忽略的是:ExceptionGroup 的树状结构意味着它可能嵌套——比如某个子异常本身又是 ExceptionGroup。这时 except* 只解一层,深层异常仍需手动递归展开,eg.exceptions 里可能混着 ValueError 和另一个 ExceptionGroup。不处理这种嵌套,等于只拆了一半包。


















