環境
- Python 3.12.4
- mypy 1.13.0
内容
dictを受け取る2つの関数があります。それぞれdict[str,str]型の値を受け取りますが、keyとvalueの型が少し異なります。
-
foo1_dict関数: dictのkeyはUnion型 -
foo2_dict関数: dictのvalueはUnion型
以下のPythonファイルに対してmypyを実行しました。
data = {"a": "b"}
def foo1_dict(arg: dict[str, str|None]) -> None: pass
# errorあり, noteあり
foo1_dict(data)
def foo2_dict(arg: dict[str|None, str]) -> None: pass
# errorあり, noteなし
foo2_dict(data)
$ mypy sample1.py
sample1.py:4: error: Argument 1 to "foo1_dict" has incompatible type "dict[str, str]"; expected "dict[str, str | None]" [arg-type]
sample1.py:4: note: "Dict" is invariant -- see https://mypy.readthedocs.io/en/stable/common_issues.html#variance
sample1.py:4: note: Consider using "Mapping" instead, which is covariant in the value type
sample1.py:8: error: Argument 1 to "foo2_dict" has incompatible type "dict[str, str]"; expected "dict[str | None, str]" [arg-type]
Found 2 errors in 1 file (checked 1 source file)
foo1_dict関数とfoo2_dict関数の呼び出しで、arg-typeのerrorメッセージが出力されました。
foo1_dict関数では、"Dict" is invariant ~というnoteメッセージが出力されましたが、foo2_dictでは出力されませんでした。
dict型からcollections.abc.Mapping型に変更する
mypyのエラーを解決するために、引数の型をdict型からcollections.abc.Mapping型に変更しました。
-
foo1_mapping関数: dictのkeyはUnion型 -
foo2_mapping関数: dictのvalueはUnion型
from collections.abc import Mapping
data = {"a": "b"}
def foo1_mapping(arg: Mapping[str, str|None]) -> None: pass
# errorなし
foo1_mapping(data)
def foo2_mapping(arg: Mapping[str|None, str]) -> None: pass
# errorあり, noteなし
foo2_mapping(data)
$ mypy sample2.py
sample2.py:10: error: Argument 1 to "foo2_mapping" has incompatible type "dict[str, str]"; expected "Mapping[str | None, str]" [arg-type]
Found 1 error in 1 file (checked 1 source file)
foo1_mapping関数の呼び出しのerrorは解決されましたが、foo2_mapping関数の呼び出しのerrorは残ってままでした。
Mapping[K,V]のKはinvariant
Mapping[K,V]のVはcovariantですが、Mapping[K,V]のKはinvariantです。
Mapping[K,V]のKがinvariantであるのは、以下のサンプルコードで理解できるかもしれません。
私は完全には理解できていません。。。
なお、過去Mapping[K,V]のKをcovariantにすることを検討していたようです。
mypyでの判定処理
以下は、mypyのnoteメッセージを出力するかどうかを判定しているコードです。dictのkey(args[0])はis_same_type、dictのvalue(args[1])はis_subtypeで型を判定していました。
elif (
arg_type.type.fullname == "builtins.dict"
and expected_type.type.fullname == "builtins.dict"
and is_same_type(arg_type.args[0], expected_type.args[0])
and is_subtype(arg_type.args[1], expected_type.args[1])
):
invariant_type = "Dict"
covariant_suggestion = (
'Consider using "Mapping" instead, which is covariant in the value type'
)
補足
記事を書いたきっかけ
loguruの以下のサンプルコードで、mypyのエラーが出たので調査しました。
level_per_module = {
"": "DEBUG",
"third.lib": "WARNING",
"anotherlib": False
}
logger.add(lambda m: print(m, end=""), filter=level_per_module, level=0)
$ mypy sample3.py
sample3.py:7: error: Argument "filter" to "add" of "Logger" has incompatible type "dict[str, object]"; expected "str | Callable[[Record], bool] | dict[str | None, str | int | bool] | None"
[arg-type]
Found 1 error in 1 file (checked 1 source file)
参考にしたサイト