深入解析 `assert_never`:用 ty 实现 Python 穷尽性检查与类型断言

发布时间:2026/9/10 10:48:43
深入解析 `assert_never`:用 ty 实现 Python 穷尽性检查与类型断言 深入解析assert_never用 ty 实现 Python 穷尽性检查与类型断言【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff导读assert_never是typing_extensions提供的运行时断言函数其核心语义是确保传入参数的类型必须是Never即不可达的底部类型否则类型检查器本仓库中为 ty位于 crates/ty_python_semantic会抛出type-assertion-failure诊断。它是 Python 开发者实现穷尽性检查exhaustiveness checking的标准工具当isinstance链或match语句穷尽所有分支后else/case _分支中的变量类型会被收窄为Never此时调用assert_never即可在编译期确认所有情况均已覆盖。读完本文你将掌握assert_never的正确用法、诊断规则、返回类型语义以及它在类型收窄与match穷尽性检查中的完整实战模式。基本功能参数类型必须为Never从语义上讲assert_never做且只做一件事验证调用参数的类型是Never。如果参数类型不是Neverty 就会发出type-assertion-failure诊断。正确用法当参数的类型本身就是Never时调用是合法的不会产生任何诊断from typing_extensions import assert_never, Never, Any from ty_extensions._internal import Unknown def _(never: Never): assert_never(never) # fine这里never参数被注解为Neverty 认为该调用断言成立代码顺利通过检查。错误用法参数类型不是Never只要参数的类型不是Neverty 就会输出type-assertion-failure诊断例如from typing_extensions import assert_never, Never, Any from ty_extensions._internal import Unknown def _(): assert_never(0) # snapshot: type-assertion-failure对应的诊断快照如下可以看到 ty 明确给出了期望类型与推断类型的差异error[type-assertion-failure]: Argument does not have asserted type Never -- src/mdtest_snippet.py:5:5 | 5 | assert_never(0) # snapshot: type-assertion-failure | ^^^^^^^^^^^^^-^ | | | Inferred type of argument is Literal[0] info: Never and Literal[0] are not equivalent types再来看其余几个典型失败示例。整数def _(): assert_never() # snapshot: type-assertion-failureerror[type-assertion-failure]: Argument does not have asserted type Never -- src/mdtest_snippet.py:7:5 | 7 | assert_never() # snapshot: type-assertion-failure | ^^^^^^^^^^^^^--^ | | | Inferred type of argument is Literal[] info: Never and Literal[] are not equivalent typesNonedef _(): assert_never(None) # snapshot: type-assertion-failureerror[type-assertion-failure]: Argument does not have asserted type Never -- src/mdtest_snippet.py:9:5 | 9 | assert_never(None) # snapshot: type-assertion-failure | ^^^^^^^^^^^^^----^ | | | Inferred type of argument is None info: Never and None are not equivalent types空元组def _(): assert_never(()) # snapshot: type-assertion-failureerror[type-assertion-failure]: Argument does not have asserted type Never -- src/mdtest_snippet.py:11:5 | 11 | assert_never(()) # snapshot: type-assertion-failure | ^^^^^^^^^^^^^--^ | | | Inferred type of argument is tuple[()] info: Never and tuple[()] are not equivalent types条件表达式即使某个分支是Never只要整体推断类型不是Never就失败def _(flag: bool, never: Never): assert_never(1 if flag else never) # snapshot: type-assertion-failureerror[type-assertion-failure]: Argument does not have asserted type Never -- src/mdtest_snippet.py:13:5 | 13 | assert_never(1 if flag else never) # snapshot: type-assertion-failure | ^^^^^^^^^^^^^--------------------^ | | | Inferred type of argument is Literal[1] info: Never and Literal[1] are not equivalent typesAnyAny与Never不等价因此同样失败def _(any_: Any): assert_never(any_) # snapshot: type-assertion-failureerror[type-assertion-failure]: Argument does not have asserted type Never -- src/mdtest_snippet.py:15:5 | 15 | assert_never(any_) # snapshot: type-assertion-failure | ^^^^^^^^^^^^^----^ | | | Inferred type of argument is Any info: Never and Any are not equivalent typesUnknownty 内部用于表示类型未知的哨兵类型同样不等价于Neverdef _(unknown: Unknown): assert_never(unknown) # snapshot: type-assertion-failureerror[type-assertion-failure]: Argument does not have asserted type Never -- src/mdtest_snippet.py:17:5 | 17 | assert_never(unknown) # snapshot: type-assertion-failure | ^^^^^^^^^^^^^-------^ | | | Inferred type of argument is Unknown info: Never and Unknown are not equivalent types上述测试用例均来自 directives/assert_never.md其中# snapshot:注释是 ty 的 mdtest 测试框架标记用于声明该行应产生的诊断。底层实现为什么参数检查不会误报invalid-argument-typeassert_never的特殊之处在于它接收的参数在常规类型检查中应当被拒绝因为普通函数不可能声明接受Never类型的实参。ty 在实现上做了专门处理避免产生与type-assertion-failure无关的噪音诊断。在 crates/ty_python_semantic/src/types.rs 中KnownFunction::AssertNever分支定义了assert_never的签名见types.rsL6388-L6404Some(KnownFunction::AssertNever) { Binding::single( self, Signature::new( Parameters::standard([Parameter::positional_only(Some( Name::new_static(arg), )) // We need to set the type to Any here (instead of Never), // in order for every assert_never call to pass the argument // check. If we set it to Never, well get invalid-argument-type // errors instead of type-assertion-failure errors. .with_annotated_type(Type::any())]), Type::Never, ), ) .into() }关键设计点有两个参数类型被刻意设为Any而非Never。源码注释写得很清楚如果把参数类型设为Never那么assert_never(0)这类调用会先触发invalid-argument-type错误而无法到达专门设计的type-assertion-failure诊断。设置为Any可以保证任何实参都能通过常规的参数类型检查从而把判断逻辑完全交给assert_never专属的诊断路径。返回类型固定为Never。无论传入什么参数调用的返回值类型永远是Never这为后续流程控制如不可达代码分析提供了依据。那么type-assertion-failure诊断本身在哪里触发在 crates/ty_python_semantic/src/types/function.rs 的KnownFunction::AssertNever分支L2575 起KnownFunction::AssertNever { let [Some(actual_ty)] parameter_types else { return; }; let env context.program_environment(); if actual_ty.is_equivalent_to(db, env, Type::Never) { return; } if let Some(builder) context.report_lint(TYPE_ASSERTION_FAILURE, call_expression) { let mut diagnostic builder.into_diagnostic(Argument does not have asserted type Never); // ... 对实参 span 附加 secondary 注解 // 展示 Inferred type of argument is ... 信息 } }逻辑非常直观先判断实参推断类型actual_ty是否与Never等价is_equivalent_to若等价则直接返回断言通过否则上报TYPE_ASSERTION_FAILURElint并以Argument does not have asserted type \Never 作为诊断标题。这解释了为什么前面所有失败示例的报错文案完全一致——它们走的是同一条代码路径。TYPE_ASSERTION_FAILURE这条 lint 规则的定义位于 crates/ty_python_semantic/src/types/diagnostic.rsL1075 附近其文档直接内嵌自 lint_docs/type-assertion-failure.md覆盖assert_type()与assert_never()两类断言失败的场景。返回类型永远是Neverassert_never的返回类型恒为Never与参数类型无关。这一点既适用于参数已是Never的情况也适用于参数类型错误的情况from typing_extensions import Never, assert_never def _(never: Never): # revealed: Never reveal_type(assert_never(never)) def _(): # revealed: Never reveal_type(assert_never(0)) # error: [type-assertion-failure]第二段代码中虽然assert_never(0)会产生type-assertion-failure错误但reveal_type揭示的返回值类型依然是Never——诊断针对的是参数断言失败返回值类型则不受影响。这为在理论上不可达的代码路径中安全地终止类型流提供了保证。实战场景一isinstance链 类型收窄的穷尽性检查assert_never最经典的用途是配合类型收窄type narrowing确认一组isinstance检查已经穷尽了所有可能的情况。当所有已知分支都被if/elif覆盖后else分支中变量的类型会被收窄为剩余类型的交集若交集为空ty 会将变量类型收窄为Never此时assert_never(obj)合法通过。以下示例需要在 Python 3.10 环境下验证mdtest 使用[environment]表声明版本[environment] python-version 3.10穷尽时检查通过from typing_extensions import assert_never, Literal class A: ... class B: ... class C: ... def if_else_isinstance_success(obj: A | B): if isinstance(obj, A): pass elif isinstance(obj, B): pass elif isinstance(obj, C): pass else: assert_never(obj)参数声明为A | B三个isinstance分支覆盖了A、B甚至多覆盖了声明类型之外的C因此else分支的剩余类型为空obj被收窄为Neverassert_never(obj)不产生任何诊断。遗漏分支时检查失败def if_else_isinstance_error(obj: A | B): if isinstance(obj, A): pass # B is missing elif isinstance(obj, C): pass else: # error: [type-assertion-failure] Type B ~A ~C is not equivalent to Never assert_never(obj)这里漏掉了B分支。else分支中obj的剩余类型是B ~A ~C即属于 B且不属于 A 也不属于 C它与Never不等价于是assert_never报错——穷尽性缺口被精确定位到具体遗漏的类型。单例比较穷尽性assert_never同样适用于基于的单例比较收窄。以下代码完整处理了Literal[1, a] | None的所有三种取值因此通过检查def if_else_singletons_success(obj: Literal[1, a] | None): if obj 1: pass elif obj a: pass elif obj is None: pass else: assert_never(obj)而一旦某个单例被拼错A代替a剩余类型Literal[a]就会暴露出来def if_else_singletons_error(obj: Literal[1, a] | None): if obj 1: pass elif obj is A: # A instead of a pass elif obj is None: pass else: # error: [type-assertion-failure] Type Literal[a] is not equivalent to Never assert_never(obj)实战场景二match语句的穷尽性检查match语句中末尾的_ as obj通配模式会绑定所有未被前面分支处理的值。如果前面的case已经穷尽所有情况obj的类型就是Never否则obj的类型就是那些遗漏值的类型并集。穷尽时检查通过from typing_extensions import Literal, assert_never def match_singletons_success(obj: Literal[1, a] | None): match obj: case 1: pass case a: pass case None: pass case _ as obj: assert_never(obj)三个case覆盖了Literal[1, a] | None的全部取值因此case _分支中的obj被收窄为Neverassert_never(obj)合法。遗漏取值时检查失败def match_singletons_error(obj: Literal[1, a] | None): match obj: case 1: pass case A: # A instead of a pass case None: pass case _ as obj: # error: [type-assertion-failure] Type Literal[a] is not equivalent to Never assert_never(obj)拼写错误的字符串模式A无法匹配a于是Literal[a]未被任何分支覆盖case _ as obj中obj的类型即为Literal[a]与Never不等价assert_never报错将拼写错误精确暴露出来。总结与最佳实践assert_never是穷尽性检查的编译期哨兵正确用法只应在理论上不可达的分支中调用如穷尽isinstance链后的else、穷尽match后的case _此时参数类型应为Never。错误语义任何非Never的实参包括Literal值、None、Any、Unknown都会触发type-assertion-failure诊断诊断信息会同时给出推断类型与不等价于Never的说明。返回值恒为Never可用于在不可达路径上终止类型流。底层机制ty 在 types.rs 中将assert_never的参数类型特判为Any以绕开常规参数检查再在 function.rs 中通过is_equivalent_to(db, env, Type::Never)判定是否满足断言——这是它区别于普通函数调用的根本所在。在维护大型 Python 代码库时将assert_never放在每个穷尽分支的末尾等于给未来新增枚举值 / 字面量却忘记处理这类回归上了一道编译期保险新增取值时类型收窄会让assert_never立刻报警帮助你定位所有需要同步修改的位置。【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考