こういうコード↓を書くと、型チェックでエラーが出ます。
from collections.abc import Hashable
def hoge1(arg1: dict[Hashable, str]):
pass
var1 = { "1": "1" }
hoge1(var1) #=> 型チェックでエラー出る
Argument of type "dict[str, str]" cannot be assigned to parameter "arg1" of type "dict[Hashable, str]" in function "hoge1"
TypeVar "_KT@dict" is invariant
"str" is incompatible with protocol "Hashable"(reportGeneralTypeIssues)
なぜでしょうか。
環境
- Python 3.10.12
- Google Colaboratory(Colab)
- Colab上の型チェックで確認していますが、pyrightでも同じような事が起きるのを確認しています。
補足: typing.Hashable is deprecated
本稿で扱うのはHashable
ですが、typing.Hashable
はPython3.12で非推奨となり、collections.abc.Hashable
を使うことが推奨されます。本稿で取り上げるエラーの話はどちらでも挙動は変わりません。
typing.Hashable
はcollections.abc.Hashable
のエイリアスなのでどちらを使っても構わなかったのですが、これから使うならばcollections.abc.Hashable
を使うのが良いでしょう。
# from typing import Hashable # Python 3.12で非推奨
from collections.abc import Hashable
エラーの概要
Hashableというのは__hash__()
メソッドが実装されていることであり、辞書(dictionary)のキーにセットされた時などに利用されます。str型が当然満たす性質です。
通常、Hashable型を指定されているところにstr型の変数は適合します。
from collections.abc import Hashable
def hoge2(arg1: Hashable):
pass
var2 = "1"
hoge(var2) # エラー出ない
ですが冒頭に挙げた通り、辞書(dictionary)のキーにHashable型を指定されていると、そこへstr型をキーに持つ辞書を渡しても適合せず型チェックでエラーになります。
def hoge1(arg1: dict[Hashable, str]):
pass
var1 = { "1": "1" }
hoge1(var1) # 型チェックでエラー出る
これは辞書のキー特有ではなく、辞書の値やリスト等に対しても同様のエラーが出ます。
def hoge3(arg1: dict[str, Hashable]):
pass
var3 = { "1": "1" }
hoge3(var3) # エラー出る
def hoge4(arg1: list[Hashable]):
pass
var4 = ["1"]
hoge4(var4) # エラー出る
Hashable
はその特性上辞書のキーとして使うことが多いような気がしますし、本問題は筆者がLangGraphのadd_conditional_edge()
の引数にある辞書のキーpath_map: Optional[Union[dict[Hashable, str], list[str]]]
で引っかかった事なので、本稿では辞書のキーを対象として話を進めます。
型チェックでエラーが出る理由
改めてエラーメッセージを見てみます。こんな一文がありました。
TypeVar "_KT@dict" is invariant
「辞書のキーはinvariantだ」と言っています。
invariantというのはvariance(変性)の話をしています。辞書のキーはmutable(可変)なコンテナなので、そこで使われている型はinvariant(非変)になります。
invariantなので、Hashable
とされていたらHashable
だけが当てはまり、str
だとエラーになるというわけです。
変性についての詳細は参考文献を参照してください。
対応方法の例
str
型で受け入れる
def hoge1(arg1: dict[str, str]):
pass
var1 = { "1": "1" }
hoge1(var1) # エラー出ない
まず身も蓋もない話ですが、Hashable
を使うのをやめてstr
で受け入れることでエラーを回避できます。
今回筆者が実際に遭遇したのはライブラリ側に関数定義があるので、このような回避方法は避けたいです。
辞書のキーをHashable
型と明示
def hoge1(arg1: dict[Hashable, str]):
pass
var1: dict[Hashable, str] = { "1": "1" }
hoge1(var1) # エラー出ない
dict[str, str]
とせずにHashable
であることを明示的に型アノテーションすれば、型が一致するためエラーは回避できます。
Hashableをupper boundとする型変数を定義
from typing import TypeVar
from collections.abc import Hashable
K = TypeVar('K', bound=Hashable)
def hoge1(arg1: dict[K, str]):
pass
var1 = { "1": "1" }
hoge1(var1) # エラー出ない
K = TypeVar('K', bound=Hashable)
とするとエラーは出なくなりました。
この解法はChatGPTに教えてもらったのですが、確かにエラーは消えたものの理由については「型チェッカーに具体的な情報を提供するとエラーを回避できる」というなんだか納得いかない事しか言ってくれませんでした。AIがエラーを直してくれるけど人類にその理解はできないとか、未来のプログラミング感ありますね。
おそらく、str
はHashable
と一致しないためエラーでしたが、「Hashable
を上界(upper bound)とする型」にはstr
が当てはまるためエラーでなくなったということでしょうか。実質的にどっちも同じことを言っているような気もしますが、Pythonの型チェックの仕様上は異なるようです。
辞書を変数に代入しない
def hoge1(arg1: dict[Hashable, str]):
pass
hoge1({ "1": "1" }) # エラー出ない
辞書を変数に代入せず直接関数に渡すと、型チェックでエラーは出ませんでした。
なぜでしょうか?理由は推測できますが、仕様がどうなっているのか私にはよく分かりませんでした。
これまで述べた通り、辞書はmutableなのでその変性はinvariant(非変)だから、エラーになります。それは辞書を変数に代入した場合の話です。
変数に代入してないリテラル時点ではimmutableだから多分covariant(共変)になり、dict[str, str]
がdict[Hashable, str]
のサブタイプとみなせるようになるからエラーは出ない、という事なのかなと思いましたが、Pythonの型チェックが実際本当にそういう仕様なのかは分かりません。