使用泛型Protocol装饰函数引发Never相关arg-type类型错误
错误成因
核心问题是自定义Protocol未正确使用ParamSpec捕获可变位置参数的签名,导致MyPy无法将传入的具体Callable类型(比如Callable[[int, int], int])与FunctionToDecorate协议匹配。当类型检查器找不到合法的类型匹配关系时,会用Never标记不兼容的部分。
为什么出现Never类型
Never是Python typing中的底部类型,代表不可能存在的类型。当MyPy无法推断出某个位置的合法类型,或者判定传入类型完全不符合协议约束时,就会用Never表示这种不兼容状态。这里因为Protocol没有准确描述无关键字参数、可变位置参数的函数结构,MyPy只能判定传入函数无法匹配协议,因此标注为FunctionToDecorate[Never, Never]。
与ParamSpec的关系
在Python 3.10+中,要准确标注带可变参数的函数(尤其是装饰器需要保留原函数签名时),必须用ParamSpec捕获函数的参数签名信息。如果Protocol没有用ParamSpec绑定可变位置参数,MyPy无法识别具体的参数类型结构,自然无法完成类型匹配。
解决方法
通过结合ParamSpec和TypeVar定义泛型Protocol,明确约束函数的参数结构(无关键字参数、可变位置参数),让MyPy能准确推断类型。
原始错误示例(MRE)
from typing import Protocol, TypeVar, Callable R = TypeVar('R') class FunctionToDecorate(Protocol[R]): def __call__(self, *args) -> R: ... def decorator(func: FunctionToDecorate[R]) -> FunctionToDecorate[R]: def wrapper(*args): print("Decorated") return func(*args) return wrapper @decorator def add(a: int, b: int) -> int: return a + b @decorator def greet(name: str) -> str: return f"Hello {name}"
MyPy错误输出:
error: Argument 1 to "decorator" has incompatible type "Callable[[int, int], int]"; expected "FunctionToDecorate[Never]" [arg-type]
error: Argument 1 to "decorator" has incompatible type "Callable[[str], str]"; expected "FunctionToDecorate[Never]" [arg-type]
修正后的代码
from typing import Protocol, TypeVar, ParamSpec, Callable # 用ParamSpec捕获位置参数签名,TypeVar捕获返回值类型 P = ParamSpec('P') R = TypeVar('R') # 定义泛型Protocol,仅接受位置参数(无关键字参数) class FunctionToDecorate(Protocol[P, R]): def __call__(self, *args: P.args) -> R: ... def decorator(func: FunctionToDecorate[P, R]) -> FunctionToDecorate[P, R]: def wrapper(*args: P.args) -> R: print("Decorated") return func(*args) return wrapper @decorator def add(a: int, b: int) -> int: return a + b @decorator def greet(name: str) -> str: return f"Hello {name}" # 若尝试传入关键字参数,MyPy会直接报错(符合需求) # add(a=1, b=2) # 错误:Unexpected keyword argument "a" for "add"
修正要点
- 引入
ParamSpec('P')捕获原函数的位置参数签名,替代模糊的*args定义; - 将Protocol改为双泛型,同时接受参数签名
P和返回值R; - 在装饰器的wrapper函数中,用
P.args约束参数类型,确保与原函数签名一致; - 若需严格禁止关键字参数,Protocol的
__call__方法不定义**kwargs即可,MyPy会自动拦截带关键字参数的函数。
内容的提问来源于stack exchange,提问作者vmonteco

