首页
/ ty 的 type-assertion-failure 规则:assert_type 与 assert_never 类型断言失败的检测原理、实现与诊断输出

ty 的 type-assertion-failure 规则:assert_type 与 assert_never 类型断言失败的检测原理、实现与诊断输出

2026-09-09 15:03:16作者:滑思眉Philip

type-assertion-failure 是 ty(Astral 出品的 Python 类型检查器,随 ruff 仓库一起维护)中最核心的静态断言类诊断之一。本文围绕该规则的官方文档展开,说明它的检查对象(typing.assert_type()typing.assert_never())、判定标准(类型“等价”而非“兼容”)、在源码中的完整实现链路(从规则注册到 check_call 中的分支逻辑),以及诊断消息的具体构成与真实快照输出,帮助你在阅读 ty 报错时准确区分它与易混淆的 assert-type-unspellable-subtype 规则。

1. 规则概览:检查对象与动机

规则文档位于 type-assertion-failure.md,其官方定义如下:

  • What it does:检查那些“实际类型与断言类型不一致”的 assert_type()assert_never() 调用;
  • Why is this badassert_type() 的用途是确认某个值被推断出的类型(inferred type)是否符合预期,一旦断言失败,通常意味着类型标注、类型收窄(narrowing)或泛型推断出了与开发者预期不符的结果。

官方文档给出的最小示例(python-version = "3.11" 环境下):

[environment]
python-version = "3.11"
from typing import assert_type


def _(x: int):
    assert_type(x, int)  # fine
    # Actual type does not match asserted type
    assert_type(x, str)  # error

从规则注册代码看(diagnostic.rs),该规则的关键元信息为:

  • 规则名(标识符):TYPE_ASSERTION_FAILURE,对外暴露的规则代码即 type-assertion-failure
  • 摘要:detects failed type assertions
  • 状态:自 0.0.1-alpha.1 起即为 stable,是 ty 最老的一批规则之一;
  • 默认级别:Level::Error,即默认开启、默认按错误报告。

值得注意的实现细节是:lint 文档文件并非独立维护的 Markdown,而是通过 declare_lint! 宏中的 #[doc = include_str!("../../resources/lint_docs/type-assertion-failure.md")] 直接内嵌进规则定义,规则文档、summary 与默认级别在编译期就被绑定在一起(diagnostic.rs)。规则随后在注册表中登记(registry.register_lint(&TYPE_ASSERTION_FAILURE),见 diagnostic.rs),并在 ty.schema.json 中作为可配置的规则代码出现,这意味着它可以在 ty 的规则级别配置中按名称被调整。

2. 实现原理:assert_type 与 assert_never 的两条检测路径

两条检测路径都实现在已知函数(known function)的调用检查逻辑中:function.rs 的 KnownFunction::check_call。ty 对 typing 模块中的特殊函数做了专门处理,assert_typeassert_never 各自对应一个 KnownFunction 分支。

2.1 assert_type:基于“类型等价”而非“可赋值性”

对应实现为 KnownFunction::AssertType 分支(function.rs),其判定流程可以归纳为三步:

  1. 取实际类型与断言类型。从重载绑定的参数类型中取出 val 的推断类型 actual_tytyp 参数 asserted_ty;断言侧的类型形式会先经过 project_type_form 投影(例如把 Type[int] 这类写法归一化,见下文测试中的 Type[int] 示例)。

  2. 等价性判定。若 actual_ty.is_equivalent_to(db, env, asserted_ty) 成立,直接放行——这正是文档中“必须精确匹配(precisely match)”语义的落点:是等价(equivalent),不是子类型(subtype),也不是可赋值(assignable)

  3. 失败后区分两种诊断。如果不等价,源码会进一步判断:

    let diagnostic = if actual_ty.is_spellable(db)
        || !actual_ty.is_subtype_of(db, env, asserted_ty)
    {
        &TYPE_ASSERTION_FAILURE
    } else {
        &ASSERT_TYPE_UNSPELLABLE_SUBTYPE
    };
    

    也就是说:只要实际类型是“可拼写的”(spellable,能用标准 Python 类型标注表达),或者实际类型根本不是断言类型的子类型,就报告 type-assertion-failure;只有当实际类型既是断言类型的子类型、又无法用标准标注表达(比如类型收窄产生的 Foo & Bar 交集类型)时,才改报兄弟规则 assert-type-unspellable-subtype。该文档明确解释了这样拆分的原因:便于用户区分“断言写错了、可以修复”与“ty 的非标准类型系统推导出更精确类型”这两类场景。

诊断消息由四部分拼装而成(function.rs):

  • 主消息:Argument does not have asserted type '{断言类型}'
  • 次级标注(secondary annotation):指向 val 实参位置,消息为 Inferred type is '{推断类型}'
  • info 行:若实际类型是断言类型的子类型,输出 '{推断类型}' is a subtype of '{断言类型}', but they are not equivalent;否则输出 '{断言类型}' and '{推断类型}' are not equivalent types
  • 简洁消息(concise message):Type '{推断类型}' does not match asserted type '{断言类型}',用于需要单行摘要的展示场景(如 LSP 悬浮或行内提示)。

2.2 assert_never:与 Never 的等价判定

KnownFunction::AssertNever 分支(function.rs)的逻辑更直接:取第一个参数 arg 的推断类型,若其与 Type::Never 等价则放行;否则报告同一个 TYPE_ASSERTION_FAILURE 规则,消息模板为:

  • 主消息:Argument does not have asserted type Never``;
  • 次级标注:Inferred type of argument is '{推断类型}'
  • info 行:'Never' and '{推断类型}' are not equivalent types
  • 简洁消息:Type '{推断类型}' is not equivalent to Never``。

这与文档“Checks for assert_type() and assert_never() calls where the actual type is not the same as the asserted type”的表述完全对应:assert_never 只是把“断言类型”固定为 Never 的特例,共用同一条规则码。

3. 诊断输出与测试证据

ty 采用 mdtest(Markdown 内嵌测试 + 快照断言)来固化每个规则的行为,type-assertion-failure 的权威测试集中在 assert_type.mdassert_never.md。以下示例均可在测试文件中逐字复现。

3.1 基本失败:int 断言成 str

from typing_extensions import assert_type

def _(x: int, y: bool):
    assert_type(x, int)  # fine
    assert_type(x, str)

快照输出(摘自测试文件):

error[type-assertion-failure]: Argument does not have asserted type `str`
 --> src/mdtest_snippet.py:6:5
  |
6 |     assert_type(x, str)
  |     ^^^^^^^^^^^^-^^^^^^
  |                 |
  |                 Inferred type is `int`
info: `str` and `int` are not equivalent types

下划线同时覆盖整条调用与被断言的类型实参,与 2.1 节描述的“主消息 + secondary 标注 + info 行”结构一一对应。

3.2 子类型不算通过:bool 断言成 int

def _(x: int, y: bool):
    assert_type(assert_type(x, int), int)
    assert_type(y, int)
error[type-assertion-failure]: Argument does not have asserted type `int`
  --> src/mdtest_snippet.py:10:5
  |
10 |     assert_type(y, int)
  |     ^^^^^^^^^^^^-^^^^^^
  |                 |
  |                 Inferred type is `bool`
info: `bool` is a subtype of `int`, but they are not equivalent

这里可以看到源码中“是子类型但非等价”那条 info 分支的真实输出(bool is a subtype of int, but they are not equivalent)。测试文件还验证了 type[int]type[Any] 互断言同样失败,以及 Type[int]type[int]project_type_form 归一化而视为等价(# fine)。

3.3 对照收窄后的推断类型,而非声明类型

[environment]
python-version = "3.10"
def _(x: int | str):
    if isinstance(x, int):
        reveal_type(x)  # revealed: int
        assert_type(x, int)  # fine

文档中的“实际类型 vs 声明类型”原则在这里得到验证:x 的声明类型是 int | str,但断言针对的是收窄后的推断类型 int,因此通过。

3.4 与 assert-type-unspellable-subtype 的分界

同组测试给出了三分支的完整演示:

class Foo: ...
class Bar: ...
class Baz: ...

def f(x: Foo):
    assert_type(x, Bar)   # error: type-assertion-failure(Foo 不是 Bar 的子类型)

    if isinstance(x, Bar):
        assert_type(x, Bar)  # error: assert-type-unspellable-subtype
                            # 推断类型是 `Foo & Bar`,是 Bar 的子类型但无法用标准标注表达

        assert_type(x, Baz)  # error: type-assertion-failure
                            # `Foo & Bar` 不是 Baz 的子类型,仍走主规则

这正对应 function.rsis_spellable || !is_subtype_of 的条件:交集类型 Foo & Bar 虽不可拼写,但它相对 Baz 不是子类型,所以依然报 type-assertion-failure

3.5 其他被测试覆盖的等价性细节

assert_type.md 还固化了若干“等价”的边界行为,均与源码的 is_equivalent_to 判定直接相关:

  • 渐进类型Any 与 ty 内部的 Unknown 被视为等价,assert_type(a: Unknown, Any)assert_type(b: Any, Unknown) 都通过;
  • 元组tuple[int, str, bytes] 与自身通过,元素顺序不同或长度不同(tuple[int, str]tuple[int, str, bytes, None]tuple[int, bytes, str])均报 type-assertion-failure
  • 联合类型str | intint | str 视为同一类型,顺序无关;
  • 交集类型Intersection[A, B, Not[C], Not[D]] 与调换顺序的写法视为等价,与 A & B & ~C & ~D 的收窄结果匹配;
  • 枚举补集F(A, B, C) 枚举排除 F.AF.B 后剩余类型表示为 Literal[F.C],断言 F 时失败并提示 Type Literal[F.C]does not match asserted typeF``——测试注释特别指出“等价于字面量联合的紧凑枚举补集仍视为可拼写”,因此走主规则而不是 unspellable 分支。

4. 规则定位小结

维度 内容 依据
触发对象 typing.assert_type(val, typ)typing.assert_never(arg) 调用 function.rs
判定标准 推断类型与断言类型必须等价(equivalent);子类型、可赋值均不通过 同文件 is_equivalent_to 判定
兄弟规则 实际类型为不可拼写的子类型时改报 assert-type-unspellable-subtype assert-type-unspellable-subtype.mdfunction.rs
默认级别 / 状态 Error / stable(自 0.0.1-alpha.1) diagnostic.rs
行为测试 mdtest 快照(基本失败、子类型、收窄、unspellable 分界、元组/联合/交集等价性) assert_type.mdassert_never.md

从源码结构看,type-assertion-failurereveal_typeassert_never 一样,属于 ty 对 typing 模块“检查指令(directives)”做静态求值的一部分:这些调用在类型检查阶段就被专门解释,而不是按普通函数调用处理。理解了这一点,再阅读 2.1 节的三分支判定逻辑与 3 节中的快照输出,就能把每一条 ty 报错与 function.rs 中的具体代码路径对应起来。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
docsdocs
暂无描述
Markdown
899
5.83 K
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.76 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
860
1.35 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
925
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.84 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
533
601
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
395
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.04 K
525