news 2026/9/10 10:48:38

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
深入解析 `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_nevertyping_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-failure
error[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 types

None

def _(): assert_never(None) # snapshot: type-assertion-failure
error[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-failure
error[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-failure
error[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 types

AnyAnyNever不等价,因此同样失败):

def _(any_: Any): assert_never(any_) # snapshot: type-assertion-failure
error[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 types

Unknown(ty 内部用于表示"类型未知"的哨兵类型,同样不等价于Never):

def _(unknown: Unknown): assert_never(unknown) # snapshot: type-assertion-failure
error[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() }

关键设计点有两个:

  1. 参数类型被刻意设为Any而非Never。源码注释写得很清楚:如果把参数类型设为Never,那么assert_never(0)这类调用会先触发invalid-argument-type错误,而无法到达专门设计的type-assertion-failure诊断。设置为Any可以保证任何实参都能通过常规的参数类型检查,从而把判断逻辑完全交给assert_never专属的诊断路径。
  2. 返回类型固定为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分支覆盖了AB,甚至多覆盖了声明类型之外的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 objobj的类型即为Literal["a"],与Never不等价,assert_never报错,将拼写错误精确暴露出来。


总结与最佳实践

assert_never是"穷尽性检查"的编译期哨兵:

  • 正确用法:只应在理论上不可达的分支中调用(如穷尽isinstance链后的else、穷尽match后的case _),此时参数类型应为Never
  • 错误语义:任何非Never的实参(包括Literal值、NoneAnyUnknown)都会触发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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 10:48:33

YOLOv9+DeepSort目标跟踪:从原理到调优的完整指南

简介:这是基于YOLOv9与DeepSORT构建的目标检测与多目标跟踪Python源码项目,面向计算机视觉毕业设计、课程项目或实战学习者,解决将检测与跟踪串联落地的核心问题。压缩包共8个文件,包含Python主脚本、Jupyter Notebook交互教程、Y…

作者头像 李华
网站建设 2026/9/10 10:43:06

Flipper 红外代码批量导入:三步搭起万能遥控器

Flipper 红外代码批量导入:三步搭起万能遥控器 【免费下载链接】Flipper Playground (and dump) of stuff I make or modify for the Flipper Zero 项目地址: https://gitcode.com/GitHub_Trending/fl/Flipper 客厅五台设备、五个遥控器,出门却只…

作者头像 李华
网站建设 2026/9/10 10:41:40

CANN/ge获取推理上下文API

GetInferenceContext 【免费下载链接】ge GE(Graph Engine)是面向昇腾的图编译器和执行器,提供了计算图优化、多流并行、内存复用和模型下沉等技术手段,加速模型执行效率,减少模型内存占用。 GE 提供对 PyTorch、Tenso…

作者头像 李华
网站建设 2026/9/10 10:40:48

LEO卫星链路级仿真:从OFDM到多普勒补偿的完整实现

我第一次在MATLAB里把LEO卫星链路的完整仿真跑通,看到星座图从一团高速旋转的乱码恢复成清晰的16QAM点阵时,确实有种“终于把理论串起来”的踏实感。这个项目本身并不是什么高深的算法创新,而是一条面向6G星地融合NTN场景的完整物理层仿真链路…

作者头像 李华