You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何让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定义的签名和实际实现的签名不兼容,具体问题点:

  1. 多余的*argv:Protocol里的方法定义了可变位置参数*argv,但json的方法只有仅限关键字参数(*分隔符后的参数),MySerializer的方法也完全不接受额外位置参数。
  2. 参数类型范围不匹配:MySerializer.loads只接受str,但Protocol要求str|bytes;json.dump的fp参数只接受SupportsWrite[str],但Protocol要求SupportsWrite[str|bytes]。
  3. 仅限关键字参数兼容问题:json的方法使用了*分隔符标记仅限关键字参数,而Protocol没有定义这一点,导致mypy判定签名不匹配。
  4. 参数名不影响匹配,但类型和参数格式必须对齐:比如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")

关键修改点说明

  1. 移除*argv:实际实现都不接受可变位置参数,保留*argv会导致mypy判定不满足Protocol要求。
  2. 对齐参数类型范围:
    • SerializerToString.loads的参数类型改为Union[str, bytes, bytearray],匹配json.loads的实际参数类型。
    • SerializerToFile.dump的file参数改为SupportsWrite[str],匹配json.dump只接受字符串写入对象的实际逻辑。
  3. 调整MySerializer方法:让dumps支持**kwargs,loads接受str|bytes类型,确保和Protocol定义对齐。
  4. 保留**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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.27 22:19:55