深入解析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。如果参数类型不是Never,ty 就会发出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参数被注解为Never,ty 认为该调用"断言成立",代码顺利通过检查。
错误用法:参数类型不是Never
只要参数的类型不是Never,ty 就会输出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 typesNone:
def _(): 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 typesAny(Any与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 typesUnknown(ty 内部用于表示"类型未知"的哨兵类型,同样不等价于Never):
def _(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-type
assert_never的特殊之处在于:它接收的参数在"常规类型检查"中应当被拒绝(因为普通函数不可能声明接受Never类型的实参)。ty 在实现上做了专门处理,避免产生与type-assertion-failure无关的噪音诊断。
在 crates/ty_python_semantic/src/types.rs 中,KnownFunction::AssertNever分支定义了assert_never的签名(见types.rsL6388-L6404):
Some(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`, we'll 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.rs(L1075 附近),其文档直接内嵌自 lint_docs/type-assertion-failure.md,覆盖assert_type()与assert_never()两类断言失败的场景。
返回类型:永远是Never
assert_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被收窄为Never,assert_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被收窄为Never,assert_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),仅供参考