Python类型检查兼容问题:如何让Mypy与Pylance同时识别字典键的Literal类型
我最近碰到个类型检查的棘手问题,场景是这样的:我写了一段调用API客户端的代码,这个API方法有一大堆可选参数,但我这边只有两种调用场景,每种场景只用其中一个不同的可选参数。两种场景的处理逻辑大部分都一致,我不想重复代码,就写了这样的简化版:
def call_api(data: list[str]): keyword, value = ("spam", "foo") if condition(data) else ("eggs", "bar") kwargs = {keyword: [value]} api_client.method(**kwargs) something(other, stuff)
结果Pylance直接报错,说list[str]类型的值对大多数其他可选参数无效——这倒是实话,因为那些参数预期的是str类型。要是把两种场景拆成两个函数硬编码参数名,确实能解决问题,但那样会重复一堆代码,实在不优雅。
后来我给代码加了类型注解,想让类型检查器搞清楚实际情况:
def call_api(data: list[str]): keyword, value = ("spam", "foo") if condition(data) else ("eggs", "bar") kwargs: dict[typing.Literal["spam", "eggs"], list[str]] = {keyword: [value]} api_client.method(**kwargs)
这下Pylance安静了,它知道字典的键只能是"spam"或者"eggs",而这两个参数确实接受list[str]类型的值。但好景不长,Mypy又开始抱怨:
error: Dict entry 0 has incompatible type "str": "list[str]"; expected "Literal['spam', 'eggs']": "list[str]"
我现在的需求是:怎么给代码加注解,既能让Mypy识别出字典的键一定是"spam"或"eggs",又不破坏Pylance需要的类型信息?我用的是Python 3.9,这个版本信息可能有帮助。
后续跟进
根据STerliakov@的建议,我把代码改成了这样:
def call_api(data: list[str]): keyword: typing.Literal["spam", "eggs"] value: str keyword, value = ("spam", "foo") if condition(data) else ("eggs", "bar") kwargs: dict[typing.Literal["spam", "eggs"], list[str]] = {keyword: [value]} api_client.method(**kwargs)
看起来我必须给kwargs加这个重复的类型注解才能满足Pylance——显然Mypy只关心单个变量的注解,而Pylance只认字典的注解。但现在又出问题了:我们的构建系统里(调试日志显示是Mypy,但我单独用Mypy跑却复现不了,可能是因为我们的包装太复杂)报错了:
Incompatible types in assignment (expression has type "str", variable has type "Literal['spam', 'eggs']") [assignment] key, value = ("spam", "foo") if condition(data) else ("eggs", "... ^
但如果我把三元表达式改成if-else分支,代码就能被正常接受:
def call_api(data: list[str]): keyword: typing.Literal["spam", "eggs"] value: str if condition(data): keyword, value = "spam", "foo" else: keyword, value = "eggs", "bar" kwargs: dict[typing.Literal["spam", "eggs"], list[str]] = {keyword: [value]} api_client.method(**kwargs)
这就很奇怪了,这两种写法逻辑上完全等价啊?还是说我碰到了类型检查器的bug?
备注:内容来源于stack exchange,提问作者user3553031

