問題
関数の引数として中身がUnion
型のdict
やlist
を定義した場合に、関数の呼び出し部分でmypyの型エラーが発生しました。直感的にはこれらの型は包含関係にありそうですが、どうやらdictやlistの型は奥が深いようです。
def func_dict(dic: dict[int|str, str]) -> None:
pass
def func_list(ls: list[int|str]) -> None:
pass
dic = {'a': 'b'}
ls = ['a', 'b']
func_dict(dic)
func_list(ls)
error: Argument 1 to "func_dict" has incompatible type "dict[str, str]"; expected "dict[int | str, str]" [arg-type]
error: Argument 1 to "func_list" has incompatible type "list[str]"; expected "list[int | str]" [arg-type]
原因
結論から言うと、str
とint|str
など中身の型自体に包含関係があっても、それらを要素とするdict
やlist
などの invariantなGenericsどうしは包含関係を失うことが原因です。以降、この詳細を解説していきます。
Genericsについて
まず、Pythonのdict
やlist
はGenericsを用いて表現されているコンテナの1つです。Genericsとは、データ型を表現する型の一部を変数化する機能です。これにより、要素に様々な型を定義できるようになっています。type hintを書くときにlist[]
のように[]
を用いてかかれる型がGenericsにあたります。このコンテナを扱う上での大きな注意点は、中身の型のsubtype関係を必ずしも引き継がないことです。具体的には、型Aが型Bのsubtype (以降 A ⊂ B とかく)だった場合でも、必ずしもGenerics[A] ⊂ Generics[B]ではないということです。この点が今回のエラーの発生要因となってくるので、以下で詳しく解説していきます。
Genericsのsubtype関係
PEP483[1] によるとGenericsどうしのsubtype関係は以下の3つに分類されるそうです。
-
covariance (共変)
- 中身のサブタイプの親子関係を受け継ぐ
->A ⊂ B
のときGenerics[A] ⊂ Generics[B]
- 例: Sequence, Union, FrozensSet
- 中身のサブタイプの親子関係を受け継ぐ
-
contravariance (反変)
- 中身のサブタイプの親子関係を逆転した関係になる
->A ⊂ B
のときGenerics[A] ⊃ Generics[B]
- 例: Callable の argument type
- 中身のサブタイプの親子関係を逆転した関係になる
-
invariance (非変)
- 中身のサブタイプ関係を完全に引き継がない
->A ⊂ B
のときGenerics[A] ⊂ Generics[B]
でもGenerics[A] ⊃ Generics[B]
でもない - 例: List, Dict
- 中身のサブタイプ関係を完全に引き継がない
基本的に Sequence
等のimmutableな (値の追加や置換ができない)インターフェースを持つデータ型はcovarianceですが、dict
やlist
などmutableなデータ型はinvarianceとなっています。str
とint|str
は subtype関係 ではないですが、同様の仕組みで、dict
等の箱に入れることによって包含関係が失われると考えるのが自然だと思います。
解決方法の例
1. 引数に入れる変数の型を明示する[2]
引数に代入する変数の型を dict[int|str, str]
などとアノテーションしてしまえば、関数で定義した型と一致するためエラーがでることはありません。ただ関数を呼び出す度に、型ヒントを確認したり型エイリアスをインポートする手間がかかるのがデメリットです。
dic : dict[int|str, str] = {'a': 'b'}
ls : list[int|str] = ['a', 'b']
func_dict(dic)
func_list(ls)
2.dictやlist自体のUnion型で定義する
Union
はcovarianceなGenericsなので、中身でなくlist
やdict
自体のUnion
型で定義してしまえばmypyは通ります。ただ、とりうる型のバリエーションが増えると、かなり冗長になってしまいます。
def func_dict(dic: dict[int, str] | dict[str, str]) -> None:
pass
def func_list(ls: list[int] | list[str]) -> None:
pass
3.中身の型を自作のGenericsで定義する
int|str
は invariantな箱の中では int や str そのものとは違う型として認識されてしまう一方で、 TypeVar('T', str, int)
という自作Genericsは、int
やstr
そのものになれる変幻自在の型を意味するため、dict
の中であってもmypyエラーが起きません。
from typing import TypeVar
T = TypeVar('T', str, int)
def func_dict(dic: dict[T, str]) -> None:
pass
def func_list(ls: list[T]) -> None:
pass
4. dictやlistをcovariantなGenericsに置き換える[2]
dict
やlist
の抽象基底クラスであるMapping
やSequence
型はcovariantであるため、それに置き換えれば問題は解決します。しかし、それらは同時にimmutableもであるため、関数内で要素を追加・削除したい場合にはこの方法は使えません。
from typing import Mapping, Sequence
def func_dict(dic: Mapping[str|int, str] -> None:
pass
def func_list(ls: Sequence[str|int]) -> None:
pass
参考
[1] PEP483 - The Theory of Type Hints, 'Covariance and Contravariance', https://peps.python.org/pep-0483/#covariance-and-contravariance
[2] mypy - Common issues and solutions, 'Invariance vs covariance', https://mypy.readthedocs.io/en/stable/common_issues.html#invariance-vs-covariance