
ty 类型检查器 Typing FAQ 深度解析Unknown、Divergent、不变性泛型与疑难报错实战指南【免费下载链接】tyAn extremely fast Python type checker and language server, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ty2/tyty 是一个用 Rust 编写的 Python 类型检查器其 pyproject.toml 中的定位是 An extremely fast Python type checker, written in Rust。本文以官方 Typing FAQ 为骨架系统讲解 ty 类型系统中几类特殊类型标记Unknown、Todo、Divergent、Top[list[Unknown]]、float*/complex*的语义与出现时机并结合仓库文档与配置项展开泛型不变性、Callable属性访问、严格模式、导入解析、Monorepo、PEP 723 等高频问题的成因与解决方案。读完本文你将能看懂 ty 诊断输出中各类晦涩类型理解报错背后的底层原理并能在 pyproject.toml 中落地对应的配置修复。开始之前安装与运行 tyty 的官方安装推荐使用 uv见 docs/installation.mduv tool install tylatest升级时执行uv tool upgrade ty。核心子命令是ty check其用法详见 docs/reference/cli.mdty check # 检查项目根目录默认 ty check src/ tests/ # 检查指定文件或目录 ty check --project packages/package-a # 指定项目目录 ty check -v # 显示详细输出含导入搜索路径报错了先做什么按错误码查阅规则文档FAQ 给出的第一条通用建议是当 ty 对你的代码报错时先查阅对应错误码的规则文档其中通常会解释问题的成因。ty 的每条诊断都对应一条具名规则完整清单位于 docs/reference/rules.md例如unresolved-import模块无法解析unresolved-attribute属性访问不存在invalid-argument-type实参类型与形参不兼容possibly-unresolved-reference变量可能未定义。每条规则的严重级别可在 docs/reference/configuration.md#rules 下统一配置取值为ignore关闭、warn警告或error错误[tool.ty.rules] possibly-unresolved-reference warn division-by-zero ignore默认情况下只要产生任何warn或error级诊断ty进程就以退出码 1 结束将terminal.error-on-warning设为false可让仅含警告的诊断以退出码 0 结束。Unknown无法完整推断出的类型Unknown是 ty 表示类型无法被完整推断的标记。它在行为上与Any完全一致属于动态/渐进类型允许对其做任何操作但区别在于出现方式Any来自用户显式注解而Unknown是 ty 隐式产生的from missing_module import MissingClass # error: unresolved-import reveal_type(MissingClass) # Unknown除了独立出现Unknown还经常以联合类型的形式参与类型推断。ty 用Unknown参与联合来避免在无类型标注的代码中产生误报同时尽可能保留有用的类型信息。FAQ 给出了一个典型场景——一个来自第三方依赖、你无法控制源码的未标注Message类class Message: data None def __init__(self, title): self.title title def receive(msg: Message): reveal_type(msg.data) # Unknown | None msg Message(Favorite color) msg.data {color: blue}这里的data属性没有任何类型注解进一步约束ty 将其类型推断为Unknown | None。联合中的Unknown让 ty 不对msg.data …赋值报错而None则如实反映了data有可能是None的事实因此任何使用msg.data的代码都必须显式处理None的情况。这一点与 docs/features/type-system.md 中描述的交叉类型能力相互配合当对Unknown类型的值做isinstance收窄时ty 会生成Unknown Iterable这类交叉类型既允许你按可迭代对象使用该值又保留原未知类型上的属性访问能力。Todoty 自身尚未实现的功能缺口Todo是 ty 用来表示因为 ty 已知的某个缺失特性或不完整实现暂时无法精确推断的类型。它与Any、Unknown一样属于动态类型ty 允许对其执行任意操作。它与两者的差异在于来源与Any不同Todo不来自显式注解与Unknown不同Todo不代表被检查代码中缺失类型信息而是ty 本身的局限。Todo可能出现在类型提示、reveal_type()输出或诊断信息中。它是 ty 的内部类型不能用于注解ty 官方计划在实现对应缺失功能后逐步消除所有Todo。Divergent不收敛的类型级递归类型推断本身可能是递归的例如循环结束时变量的类型取决于循环开始时的类型。ty 会反复分析这类循环依赖寻找稳定结果如果每一轮迭代都产生一个新类型、始终无法收敛ty 就会把不收敛的部分替换为Divergent。FAQ 给出了一个典型例子——每轮循环都把x多包一层列表def some_condition() - bool: ... x 1 while some_condition(): x [x] reveal_type(x) # Literal[1] | list[Divergent]第一轮分析后x可能是Literal[1]或list[Literal[1]]下一轮又追加list[list[Literal[1]]]此后每一轮都增加一层嵌套无法收敛为有限类型。reveal_type因此保留已知的基例Literal[1]并用Divergent表示无限扩张的部分。与Any、Unknown一样Divergent是渐进类型ty 允许对类型的Divergent部分执行任意操作但与Unknown不同它不代表缺失的类型信息。它同样是 ty 的内部类型不能用于注解。为什么 ty 会显示float*或complex*Python typing 规范为数值类型设定了一条特殊规则int可以用于任何期望float的位置尽管int并非float的子类def circle_area(radius: float) - float: return 3.14 * radius * radius circle_area(2) # OK: int is allowed where float is expected由于float注解同时接受整数和真正的浮点值ty 在不知道具体值是哪种时把这个完整类型显示为float。当 ty 确定某个值确实是float而非int时则显示更精确的类型float*def takes_float(value: float) - None: reveal_type(value) # float reveal_type(1.0) # float*complex遵循类似规则complex注解接受int、float、complex三类值ty 显示为complex已知是真正complex而非int或float的值显示为complex*def takes_complex(value: complex) - None: reveal_type(value) # complex reveal_type(1j) # complex*带星号的拼写只出现在 ty 的输出中不能用于 Python 注解。只想接受真正的floatJustFloat与JustComplex在绝大多数情况下把参数标注为float时你都希望同时接受int和float。但如果确实需要只接受float、拒绝int可以使用 ty 的JustFloat类型。截至写作时该导入需要放在TYPE_CHECKING块中保护from typing import TYPE_CHECKING if TYPE_CHECKING: from ty_extensions import JustFloat else: JustFloat float def only_actual_floats_allowed(f: JustFloat) - None: ... only_actual_floats_allowed(1.0) # OK only_actual_floats_allowed(1) # error: invalid-argument-typecomplex的对应场景可使用ty_extensions.JustComplex用法相同。为什么不能把list[Subtype]传给期望list[Supertype]的参数——泛型不变性假设有Entry基类及其两个子类Directory、File。由于Directory是Entry你自然认为Directory可以在任何期望Entry的地方使用进而推断list[Directory]也应能用于期望list[Entry]的上下文。但事实并非如此原因在于可变性mutabilityfrom dataclasses import dataclass dataclass class Entry: path: str def size_bytes(self) - int: ... dataclass class Directory(Entry): def children(self) - list[Entry]: ... dataclass class File(Entry): def content(self) - bytes: ... def modify(entries: list[Entry]): entries.append(File(README.txt)) # mutation directories: list[Directory] [Directory(Downloads), Directory(Documents)] modify(directories) # ty emits an error on this callmodify会修改directories列表的内容调用之后它包含两个目录和一个File这显然违背了list[Directory]的类型注解。如果该调用被允许后续依赖directories只包含Directory实例的代码就会在运行时崩溃for directory in directories: directory.children() # runtime: File object has no attribute children用类型系统的术语说list是**不变invariant的A是B的子类型并不意味着list[A]是list[B]的子类型。set、dict等可变内置集合同理。相反只读集合如tuple、frozenset在其类型参数上是协变covariant**的——把frozenset[bool]赋给frozenset[int]是安全的因为内容不可变。只读场景下的三种解法不变性会在不涉及可变的场景也带来困扰def total_size_bytes(entries: list[Entry]) - int: return sum(entry.size_bytes() for entry in entries) # inferred as list[Directory] media_entries [Directory(Pictures), Directory(Videos)] # still a type-check error, but should be fine in principle (no mutation occurs) size total_size_bytes(media_entries)此时有几种修复路径改写函数签名把参数类型改为Sequence[Entry]。Sequence描述只读序列在其类型参数上是协变的因此上述调用不再报错——这是最推荐的做法。加宽实参类型无法修改被调用函数签名时将media_entries显式注解为list[Entry]。复制列表在某些场景下合理——total_size_bytes(list(media_entries))。如果你在寻找dict[str, V]的协变替代品可以使用Mapping[str, V]。为什么 ty 说Callable没有__name__属性对类型为Callable的值访问__name__、__qualname__、__module__或__doc__时ty 会报告unresolved-attribute错误。原因是并非所有可调用对象都拥有这些属性函数包括 lambda有但其他可调用对象没有。下面的FileUpload类实例可被调用却没有__name__属性——把FileUpload实例传给retry会在运行时触发AttributeErrorfrom typing import Callable def retry(times: int, operation: Callable[[], bool]) - bool: for i in range(times): # WRONG: operation does not necessarily have a __name__ attribute print(fCalling {operation.__name__}, attempt {i 1} of {times}) if operation(): return True return False class FileUpload: def __init__(self, name: str) - None: # … def __call__(self) - bool: # … retry(3, FileUpload(image.png))修复方案一getattr提供回退默认值name getattr(operation, __name__, operation)如果多处访问也可以改用hasattr(…, __name__)检查。修复方案二isinstance收窄到函数类型if isinstance(operation, FunctionType): print(fCalling {operation.__name__}, attempt {i 1} of {times}) else: print(fCalling operation, attempt {i 1} of {times})修复方案三利用 ty 的交叉类型一等公民支持ty 对交叉类型intersection types有一等公民的支持详见 docs/features/type-system.md。如果你只想接受函数式可调用对象可以定义Callable与types.FunctionType的交叉from typing import Callable, TYPE_CHECKING from types import FunctionType if TYPE_CHECKING: from ty_extensions import Intersection type FunctionLikeCallable[**P, R] Intersection[Callable[P, R], FunctionType] else: FunctionLikeCallable Callable def retry(times: int, operation: FunctionLikeCallable[[], bool]) - bool: ...改造后FileUpload实例不再被retry接受问题在类型检查阶段即被拦截。Top[list[Unknown]]是什么为什么会出现在收窄结果里Top[list[Unknown]]表示所有可能的、元素类型任意的列表与之相对list[Unknown]表示某个元素类型未知的列表。它通常出现在你开启analysis.strict-generic-narrowing选项、并执行if isinstance(x, list):这类检查时。考虑x之前的类型是Item | list[Item]。你可能期望该检查把类型收窄为list[Item]但 ty 尊重存在Item与list的公共子类它未必是list[Item]的可能性因此收窄后的类型是(Item Top[list[Unknown]]) | list[Item]。要写出更健壮的代码有两个办法改为检查if isinstance(x, Item)将Item声明为typing.final类彻底排除公共子类的可能。这与 docs/features/type-system.md 中top 物化top materialization的机制一致对不变泛型类而言其 top 物化无法用 Python 类型系统直接表达但它是 ty 在isinstance检查涉及泛型类且开启strict-generic-narrowing时所交叉出的有用类型。关闭该选项时ty 采用渐进式泛型收窄isinstance(value, list)会把Sequence[int]收窄为list[int]尽可能保留兼容的类型实参若无可用特化则把object收窄为list[Unknown]。该选项默认关闭[tool.ty.analysis] # Use the top materialization when narrowing to an unspecialized generic class strict-generic-narrowing truety 有严格模式吗ty 目前没有名为--strict的开关但它默认就相当严格并且有简单的方式进一步收紧检查。详见 docs/coming-from-mypy-or-pyright.md#stricter-checking-with-ty。该文档给出的近似其他类型检查器--strict的推荐配置如下[tool.ty.rules] dynamic-function-decorator-return error missing-type-argument error possibly-unresolved-reference warn unsound-return-statement error [tool.ruff.lint] extend-select [ANN, PYI] preview true其中dynamic-function-decorator-return、missing-type-argument、possibly-unresolved-reference、unsound-return-statement都是 ty 默认关闭、较为有主见的规则ANN与PYI则是 Ruff 中专注于类型注解质量的规则组。更进一步的严格配置还可以叠加strict-equality-semantics、strict-generic-narrowing以及unsound-assignment、unsound-yield、blanket-ignore-comment等规则。ty 为什么不对缺失类型注解报错ty不会对未注解的函数参数、返回值或变量报错。遇到未注解符号时ty 将其推断为Unknown同时尽可能提供其他有用的诊断。这与 FAQ 中ty 无条件检查未注解函数体的行为一致——mypy 的check_untyped_defs在 ty 中没有对应规则因为检查未注解函数体本就是 ty 的默认行为。如果你需要 mypydisallow_untyped_defs错误码no-untyped-def的等价能力Ruff 通过其flake8-annotationsANN规则组提供了可选的 lint 规则。常用规则包括ANN001函数参数缺少类型注解ANN002*args缺少类型注解ANN003**kwargs缺少类型注解ANN201公共函数缺少返回类型注解ANN202私有函数缺少返回类型注解RUF045dataclass 中存在隐式类变量。导入解析失败怎么办当 ty 报告Cannot resolve imported module …即unresolved-import规则时通常是环境配置缺失或错误所致。可按以下顺序排查虚拟环境确认虚拟环境可被发现。ty 通过VIRTUAL_ENV环境变量或项目根目录下的.venv目录查找活动虚拟环境。更完整的机制见 docs/modules.md#python-environmentty 优先检查VIRTUAL_ENV其次查找项目根目录的.venv使用 uv、Poetry 等项目管理工具时其run命令通常会自动激活虚拟环境并被 ty 检测到。项目结构如果你的源码不在项目根目录或src/目录下请在pyproject.toml中配置environment.root[tool.ty.environment] root [./app]root接受按优先级排序的目录列表第一个优先级最高。未指定时ty 会自动检测常见项目布局项目根目录.始终包含若存在且不是包不含__init__.py/__init__.pyi./src、./project-name、./python也会被加入。第三方包确认依赖已安装到虚拟环境中。运行ty check -v可查看正在使用的搜索路径。如果模块确实无法通过常规方式安装还可以使用analysis.allowed-unresolved-imports按 glob 抑制诊断或用analysis.replace-imports-with-any把匹配模块的类型信息替换为AnyPYTHONPATH中的现有目录也会按 Python 解释器相同的解析顺序加入搜索路径见 docs/reference/environment.md。编译扩展ty 需要.py或.pyi文件来获取类型信息。如果某个包只包含编译扩展.so或.pyd文件你需要为其提供.pyi桩文件ty 才能理解其类型。ty 支持 Monorepo 吗支持但对嵌套项目的自动发现能力有限。默认情况下ty 使用当前工作目录或--project选项确定项目根。对包含多个 Python 包的 Monorepo有两种方案逐包运行在每个包目录下运行ty check或使用--project指定包ty check --project packages/package-a ty check --project packages/package-b配置多个源码根用environment.root指定多个源码目录[tool.ty.environment] root [packages/package-a, packages/package-b]这种方式的缺点是把所有包视为单个项目可能导致 ty 认为某些模块可导入、而运行时实际不可导入的情况。ty 支持 PEP 723 inline-metadata 脚本吗视你的需求而定。如果只有一个带 inline-metadata 头的脚本可以用 uv 的--with-requirements标志安装脚本头声明的依赖后进行检查uvx --with-requirements script.py ty check script.py如果工作区中有多个脚本ty 目前还无法根据各自的 inline metadata 区分它们不同的依赖。另注意inline-metadata 脚本默认没有 first-party 根它们是单文件程序需要导入本地模块时设置root [.]。有 ty 的 pre-commit hook 吗有。ty 官方维护了独立的ty-pre-commit仓库可在其中找到现成的 pre-commit hook 配置示例repo: https://github.com/astral-sh/ty-pre-commit直接在 CI 或本地 pre-commit 流程中引用即可。ty 支持mypy 式插件吗不支持。ty没有插件系统目前也没有添加插件系统的计划。ty 更倾向于用定义良好的特性扩展类型系统本身而不是依赖类型检查器专属的插件。作为替代方向ty 正在考虑把 pydantic、SQLAlchemy、attrs、django 等热门第三方库的一等支持直接内建到 ty 中。延伸阅读docs/reference/rules.md全部 ty 规则的完整清单与说明docs/reference/configuration.mdrules、analysis、environment、overrides等全部配置项docs/coming-from-mypy-or-pyright.md从 mypy/pyright 迁移的规则映射表与严格模式配置docs/features/type-system.md交叉类型、top/bottom 物化、基于类型的可达性分析等类型系统特性docs/modules.mdfirst-party 与 third-party 模块发现机制docs/installation.md安装与升级方式docs/reference/cli.mdty check等 CLI 命令与参数。【免费下载链接】tyAn extremely fast Python type checker and language server, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ty2/ty创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考