为何Mypy判定Tuple[float, Optional[float]]与指定Union类型不兼容?
问题场景
以下代码中,Mypy判定传入的元组与函数参数类型不兼容:
from typing import Optional, Union def foo(arg: Union[float, tuple[float, float], tuple[float, None]]) -> None: pass a: float = 1.0 b: Optional[float] = None foo((a, b))
报错信息
$ mypy typetest.py
typetest.py:9: error: Argument 1 to "foo" has incompatible type "Tuple[float, Optional[float]]"; expected "Union[float, Tuple[float, float], Tuple[float, None]]"
用户的困惑:b被注解为Optional[float](仅能取float或None),理论上(a, b)只会是tuple[float, float]或tuple[float, None],完全匹配函数参数的Union类型,但Mypy仍报错。
实际工作场景:封装requests.Session时,timeout参数的类型注解与上述arg一致,传入Optional[float]类型参数时遇到相同问题,需明确正确处理方式。
原因分析
Mypy基于静态类型推导工作,不会运行代码判断变量的实际值。当传入(a, b)时,Mypy会推导出该元组的静态类型为Tuple[float, Optional[float]]——这是一个单一的元组类型(第二个元素为可选类型),而函数参数声明的是两个具体元组类型的联合:tuple[float, float]和tuple[float, None]。这两种类型在静态检查中并不等价,因此Mypy判定不兼容。
解决方法
针对该问题,有三种可行的处理方式:
1. 显式分支处理(推荐)
通过条件判断拆分b的两种取值场景,让Mypy能在对应分支中推导出精确的元组类型:
from typing import Optional, Union def foo(arg: Union[float, tuple[float, float], tuple[float, None]]) -> None: pass a: float = 1.0 b: Optional[float] = None if b is None: foo((a, b)) # Mypy识别为tuple[float, None] else: foo((a, b)) # Mypy识别为tuple[float, float]
2. 使用typing.cast强制类型转换
若能确保运行时b的取值仅为float或None,可通过cast告知Mypy该元组符合参数类型要求:
from typing import Optional, Union, cast def foo(arg: Union[float, tuple[float, float], tuple[float, None]]) -> None: pass a: float = 1.0 b: Optional[float] = None foo(cast(Union[tuple[float, float], tuple[float, None]], (a, b)))
3. 调整函数参数类型(仅当有权限修改时)
若可以修改函数的类型注解,将参数改为接受Tuple[float, Optional[float]],即可直接兼容:
def foo(arg: Union[float, tuple[float, Optional[float]]]) -> None: pass
注意:此方法不适用于requests的timeout参数,因为无法修改typeshed中的官方注解。
内容的提问来源于stack exchange,提问作者Nobilis

