如何让mypy认可带runtime_checkable的运行时可用Protocol
问题描述
我定义了几个带@runtime_checkable装饰器的Protocol,代码运行正常,但用mypy做类型检查时,所有赋值操作都报类型不兼容错误。
原始代码
import json from typing import Any, Protocol, TypeVar, runtime_checkable T_co = TypeVar('T_co', covariant=True) T = TypeVar('T') @runtime_checkable class SupportsRead(Protocol[T_co]): def read(self, __length: int = ...) -> T_co: ... @runtime_checkable class SupportsWrite(Protocol[T_co]): def write(self, data: str | bytes): ... @runtime_checkable class SerializerToString(Protocol): def dumps(self, value: Any, *argv, **kwargs) -> str: ... def loads(self, value: str | bytes, *argv, **kwargs) -> Any: ... @runtime_checkable class SerializerToFile(Protocol): def dump(self, value: Any, file: SupportsWrite[str | bytes], **kwargs) -> None: ... def load(self, file: SupportsRead[str | bytes], **kwargs) -> Any: ... @runtime_checkable class Serializer(SerializerToString, SerializerToFile, Protocol): pass class MySerializer: def dumps(self, value: Any) -> str: return f"dumps {value}" def loads(self, value: str) -> Any: return f"loads {value}" var1: SerializerToFile = json var2: SerializerToString = json var3: Serializer = json var4: SerializerToString = MySerializer() assert isinstance(var1, SerializerToFile) assert isinstance(var2, SerializerToString) assert isinstance(var3, Serializer) assert isinstance(var4, SerializerToString) print("everything ok")
mypy错误信息
test11.py:42: error: Incompatible types in assignment (expression has type Module, variable has type "SerializerToFile") [assignment] test11.py:42: note: Following member(s) of "Module json" have conflicts: test11.py:42: note: Expected: test11.py:42: note: def dump(value: Any, file: SupportsWrite[Union[str, bytes]], **kwargs: Any) -> None test11.py:42: note: Got: test11.py:42: note: def dump(obj: Any, fp: SupportsWrite[str], *, skipkeys: bool = ..., ensure_ascii: bool = ..., check_circular: bool = ..., allow_nan: bool = ..., cls: Optional[Type[JSONEncoder]] = ..., indent: Union[None, int, str] = ..., separators: Optional[Tuple[str, str]] = ..., default: Optional[Callable[[Any], Any]] = ..., sort_keys: bool = ..., **kwds: Any) -> None test11.py:42: note: Expected: test11.py:42: note: def load(file: SupportsRead[Union[str, bytes]], **kwargs: Any) -> Any test11.py:42: note: Got: test11.py:42: note: def load(fp: SupportsRead[Union[str, bytes]], *, cls: Optional[Type[JSONDecoder]] = ..., object_hook: Optional[Callable[[Dict[Any, Any]], Any]] = ..., parse_float: Optional[Callable[[str], Any]] = ..., parse_int: Optional[Callable[[str], Any]] = ..., parse_constant: Optional[Callable[[str], Any]] = ..., object_pairs_hook: Optional[Callable[[List[Tuple[Any, Any]]], Any]] = ..., **kwds: Any) -> Any test11.py:43: error: Incompatible types in assignment (expression has type Module, variable has type "SerializerToString") [assignment] test11.py:43: note: Following member(s) of "Module json" have conflicts: test11.py:43: note: Expected: test11.py:43: note: def dumps(value: Any, *argv: Any, **kwargs: Any) -> str test11.py:43: note: Got: test11.py:43: note: def dumps(obj: Any, *, skipkeys: bool = ..., ensure_ascii: bool = ..., check_circular: bool = ..., allow_nan: bool = ..., cls: Optional[Type[JSONEncoder]] = ..., indent: Union[None, int, str] = ..., separators: Optional[Tuple[str, str]] = ..., default: Optional[Callable[[Any], Any]] = ..., sort_keys: bool = ..., **kwds: Any) -> str test11.py:43: note: Expected: test11.py:43: note: def loads(value: Union[str, bytes], *argv: Any, **kwargs: Any) -> Any test11.py:43: note: Got: test11.py:43: note: def loads(s: Union[str, bytes, bytearray], *, cls: Optional[Type[JSONDecoder]] = ..., object_hook: Optional[Callable[[Dict[Any, Any]], Any]] = ..., parse_float: Optional[Callable[[str], Any]] = ..., parse_int: Optional[Callable[[str], Any]] = ..., parse_constant: Optional[Callable[[str], Any]] = ..., object_pairs_hook: Optional[Callable[[List[Tuple[Any, Any]]], Any]] = ..., **kwds: Any) -> Any test11.py:44: error: Incompatible types in assignment (expression has type Module, variable has type "Serializer") [assignment] test11.py:44: note: Following member(s) of "Module json" have conflicts: test11.py:44: note: Expected: test11.py:44: note: def dump(value: Any, file: SupportsWrite[Union[str, bytes]], **kwargs: Any) -> None test11.py:44: note: Got: test11.py:44: note: def dump(obj: Any, fp: SupportsWrite[str], *, skipkeys: bool = ..., ensure_ascii: bool = ..., check_circular: bool = ..., allow_nan: bool = ..., cls: Optional[Type[JSONEncoder]] = ..., indent: Union[None, int, str] = ..., separators: Optional[Tuple[str, str]] = ..., default: Optional[Callable[[Any], Any]] = ..., sort_keys: bool = ..., **kwds: Any) -> None test11.py:44: note: Expected: test11.py:44: note: def dumps(value: Any, *argv: Any, **kwargs: Any) -> str test11.py:44: note: Got: test11.py:44: note: def dumps(obj: Any, *, skipkeys: bool = ..., ensure_ascii: bool = ..., check_circular: bool = ..., allow_nan: bool = ..., cls: Optional[Type[JSONEncoder]] = ..., indent: Union[None, int, str] = ..., separators: Optional[Tuple[str, str]] = ..., default: Optional[Callable[[Any], Any]] = ..., sort_keys: bool = ..., **kwds: Any) -> str test11.py:44: note: <2 more conflict(s) not shown> test11.py:45: error: Incompatible types in assignment (expression has type "MySerializer", variable has type "SerializerToString") [assignment] test11.py:45: note: Following member(s) of "MySerializer" have conflicts: test11.py:45: note: Expected: test11.py:45: note: def dumps(self, value: Any, *argv: Any, **kwargs: Any) -> str test11.py:45: note: Got: test11.py:45: note: def dumps(self, value: Any) -> str test11.py:45: note: Expected: test11.py:45: note: def loads(self, value: Union[str, bytes], *argv: Any, **kwargs: Any) -> Any test11.py:45: note: Got: test11.py:45: note: def loads(self, value: str) -> Any Found 4 errors in 1 file (checked 1 source file)
核心疑问
如何让mypy认可这些运行时正常工作的Protocol?
错误原因分析
mypy对Protocol的方法签名匹配要求严格,报错的核心是Protocol定义的签名和实际实现的签名不兼容,具体问题点:
- 多余的
*argv:Protocol里的方法定义了可变位置参数*argv,但json的方法只有仅限关键字参数(*分隔符后的参数),MySerializer的方法也完全不接受额外位置参数。 - 参数类型范围不匹配:
MySerializer.loads只接受str,但Protocol要求str|bytes;json.dump的fp参数只接受SupportsWrite[str],但Protocol要求SupportsWrite[str|bytes]。 - 仅限关键字参数兼容问题:
json的方法使用了*分隔符标记仅限关键字参数,而Protocol没有定义这一点,导致mypy判定签名不匹配。 - 参数名不影响匹配,但类型和参数格式必须对齐:比如
value和obj/s的参数名差异不影响,但参数的可选性、类型范围必须一致。
解决方案
调整Protocol的方法签名,使其和实际实现兼容,同时保留运行时检查的有效性:
修改后的代码
import json from typing import Any, Protocol, TypeVar, runtime_checkable, Union T_co = TypeVar('T_co', covariant=True) T = TypeVar('T') @runtime_checkable class SupportsRead(Protocol[T_co]): def read(self, __length: int = ...) -> T_co: ... @runtime_checkable class SupportsWrite(Protocol[T_co]): def write(self, data: str | bytes): ... @runtime_checkable class SerializerToString(Protocol): # 移除*argv,对齐json的仅限关键字参数格式,放宽loads的参数类型范围 def dumps(self, value: Any, **kwargs) -> str: ... def loads(self, value: Union[str, bytes, bytearray], **kwargs) -> Any: ... @runtime_checkable class SerializerToFile(Protocol): # 调整file参数类型为SupportsWrite[str],匹配json的实际要求 def dump(self, value: Any, file: SupportsWrite[str], **kwargs) -> None: ... def load(self, file: SupportsRead[Union[str, bytes]], **kwargs) -> Any: ... @runtime_checkable class Serializer(SerializerToString, SerializerToFile, Protocol): pass class MySerializer: # 添加**kwargs以兼容Protocol定义 def dumps(self, value: Any, **kwargs) -> str: return f"dumps {value}" # 放宽参数类型为str|bytes,对齐Protocol要求 def loads(self, value: Union[str, bytes]) -> Any: if isinstance(value, bytes): value = value.decode() return f"loads {value}" var1: SerializerToFile = json var2: SerializerToString = json var3: Serializer = json var4: SerializerToString = MySerializer() assert isinstance(var1, SerializerToFile) assert isinstance(var2, SerializerToString) assert isinstance(var3, Serializer) assert isinstance(var4, SerializerToString) print("everything ok")
关键修改点说明
- 移除
*argv:实际实现都不接受可变位置参数,保留*argv会导致mypy判定不满足Protocol要求。 - 对齐参数类型范围:
SerializerToString.loads的参数类型改为Union[str, bytes, bytearray],匹配json.loads的实际参数类型。SerializerToFile.dump的file参数改为SupportsWrite[str],匹配json.dump只接受字符串写入对象的实际逻辑。
- 调整
MySerializer方法:让dumps支持**kwargs,loads接受str|bytes类型,确保和Protocol定义对齐。 - 保留
**kwargs:兼容json的各种配置参数,同时允许自定义Serializer忽略这些参数。
可选方案:临时忽略类型错误
如果不想修改MySerializer的实现,可以用# type: ignore临时跳过mypy检查,但这种方式会降低类型安全性,不推荐长期使用:
class MySerializer: def dumps(self, value: Any) -> str: # type: ignore[override] return f"dumps {value}" def loads(self, value: str) -> Any: # type: ignore[override] return f"loads {value}"
内容的提问来源于stack exchange,提问作者Copperfield
相关产品推荐
相关产品推荐

