Pyright实现Protocol时出现False Positive的问题咨询
我最近遇到一个Pyright的类型检查困惑,先给大家看看我的最小可复现代码(MRE):
from dataclasses import dataclass from typing import Protocol class Child(Protocol): val: float class Parent(Protocol): sub: Child @dataclass class Child1(Child): val: float @dataclass class Parent1(Parent): sub: Child1 # <- Pyright complains here
在我看来,Parent1完全是Parent协议的合法实现,但Pyright却抛出了错误,而mypy却能正常通过检查。Pyright的错误提示是:
"sub" overrides symbol of same name in class "Parent"
Variable is mutable so its type is invariant
Override type "Child1" is not the same as base type "Child" (reportIncompatibleVariableOverride)
我完全搞不懂这里的“可变性”到底指什么——我试过给数据类加上frozen=True,结果还是一样报错。为什么mypy能认可这种写法,Pyright却不行?有没有什么办法可以调整代码,或者让Pyright忽略这个看起来的误报呢?
问题原因解析
其实这不算Pyright的误报,而是两个类型检查工具对协议中可变属性的协变性处理逻辑不同:
- Pyright对协议里的普通可变属性(非
@property定义的属性)遵循**不变性(invariant)**规则:子类属性的类型必须和协议里的完全一致,不能是协议类型的子类。这是因为可变属性意味着你后续可以给它重新赋值,如果允许子类类型,之后可能会把不符合协议的对象赋值进去,破坏类型安全。 - mypy在处理协议属性时,默认会把它们当作只读属性看待,所以允许协变(也就是子类属性可以是协议属性类型的子类),这就是两者检查结果不同的核心原因。
解决方案
这里有几种可行的解决办法,你可以根据实际场景选择:
1. 将协议属性改为只读(用@property)
如果你的sub属性不需要被重新赋值,推荐把协议里的属性定义成只读的@property,这样Pyright就会允许协变了:
from dataclasses import dataclass from typing import Protocol class Child(Protocol): @property def val(self) -> float: ... class Parent(Protocol): @property def sub(self) -> Child: ... @dataclass(frozen=True) class Child1(Child): val: float @dataclass(frozen=True) class Parent1(Parent): sub: Child1
这里给数据类加上frozen=True是为了确保属性只读,和协议的定义保持一致。
2. 修改Pyright配置,关闭对应检查规则
如果你确定这种写法是安全的,不想修改代码,可以在项目的pyrightconfig.json里关闭reportIncompatibleVariableOverride规则:
{ "reportIncompatibleVariableOverride": false }
不过这种方法要谨慎使用,因为关闭该规则可能会错过真正的类型不兼容问题。
3. 明确标注协变性(适用于Pyright 1.1.300+)
从Pyright 1.1.300版本开始,支持通过TypeVar给协议属性标注协变性:
from dataclasses import dataclass from typing import Protocol, TypeVar TChild = TypeVar("TChild", bound="Child", covariant=True) class Child(Protocol): val: float class Parent(Protocol[TChild]): sub: TChild @dataclass class Child1(Child): val: float @dataclass class Parent1(Parent[Child1]): sub: Child1
这种方式可以明确告诉Pyrightsub属性的类型是协变的,从而允许子类使用Child1类型。
总结
简单来说,Pyright对可变属性的类型检查更严格,而mypy的处理相对宽松。根据你的实际需求选择合适的方案即可——如果属性是只读的,优先用第一种方法;如果必须保持可变且确认代码安全,可以考虑第二种或第三种方法。
内容来源于stack exchange

