0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

mypy: 引数の型が`dict[str|None,str]`である関数に`dict[str,str]`型の値を渡しても、`"Dict" is invariant `というnoteメッセージは出力されない

Last updated at Posted at 2024-12-11

環境

  • 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を実行しました。

sample1.py
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
sample2.py
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で型を判定していました。

mypy/mypy/messages.py
    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のエラーが出たので調査しました。

sample3.py
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)

参考にしたサイト

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?