Pythonのdict型のキーをstr型にしてHashableに入れるとエラー

こういうコード↓を書くと、型チェックでエラーが出ます。

```python
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で非推奨](https://github.com/python/cpython/issues/94309)となり、`collections.abc.Hashable`を使うことが推奨されます。本稿で取り上げるエラーの話はどちらでも挙動は変わりません。
`typing.Hashable`は`collections.abc.Hashable`のエイリアスなのでどちらを使っても構わなかったのですが、これから使うならば`collections.abc.Hashable`を使うのが良いでしょう。

```python
# from typing import Hashable # Python 3.12で非推奨
from collections.abc import Hashable
```

## エラーの概要
[Hashable](https://docs.python.org/3/library/collections.abc.html#collections.abc.Hashable)というのは`__hash__()`メソッドが実装されていることであり、辞書（dictionary）のキーにセットされた時などに利用されます。str型が当然満たす性質です。
通常、Hashable型を指定されているところにstr型の変数は適合します。

```python
from collections.abc import Hashable

def hoge2(arg1: Hashable):
    pass

var2 = "1"
hoge(var2) # エラー出ない
```

ですが冒頭に挙げた通り、辞書（dictionary）のキーにHashable型を指定されていると、そこへstr型をキーに持つ辞書を渡しても適合せず型チェックでエラーになります。

```python
def hoge1(arg1: dict[Hashable, str]):
    pass

var1 = { "1": "1" }
hoge1(var1) # 型チェックでエラー出る
```

これは辞書のキー特有ではなく、辞書の値やリスト等に対しても同様のエラーが出ます。

```python
def hoge3(arg1: dict[str, Hashable]):
    pass

var3 = { "1": "1" }
hoge3(var3) # エラー出る
```

```python
def hoge4(arg1: list[Hashable]):
    pass

var4 = ["1"]
hoge4(var4) # エラー出る
```


`Hashable`はその特性上辞書のキーとして使うことが多いような気がしますし、本問題は筆者が[LangGraphの`add_conditional_edge()`](https://github.com/langchain-ai/langgraph/blob/cli%3D%3D0.1.41/libs/langgraph/langgraph/graph/graph.py#L181)の引数にある辞書のキー`path_map: Optional[Union[dict[Hashable, str], list[str]]]`で引っかかった事なので、本稿では辞書のキーを対象として話を進めます。

## 型チェックでエラーが出る理由
改めてエラーメッセージを見てみます。こんな一文がありました。

>TypeVar "_KT@dict" is invariant

「辞書のキーはinvariantだ」と言っています。

invariantというのはvariance(変性)の話をしています。辞書のキーはmutable（可変）なコンテナなので、そこで使われている型はinvariant（非変）になります。
invariantなので、`Hashable`とされていたら`Hashable`だけが当てはまり、`str`だとエラーになるというわけです。

変性についての詳細は参考文献を参照してください。

## 対応方法の例
### `str`型で受け入れる

```python
def hoge1(arg1: dict[str, str]):
    pass

var1 = { "1": "1" }
hoge1(var1) # エラー出ない
```

まず身も蓋もない話ですが、`Hashable`を使うのをやめて`str`で受け入れることでエラーを回避できます。

今回筆者が実際に遭遇したのはライブラリ側に関数定義があるので、このような回避方法は避けたいです。

### 辞書のキーを`Hashable`型と明示

```python
def hoge1(arg1: dict[Hashable, str]):
    pass

var1: dict[Hashable, str] = { "1": "1" }
hoge1(var1) # エラー出ない
```

`dict[str, str]`とせずに`Hashable`であることを明示的に型アノテーションすれば、型が一致するためエラーは回避できます。

### Hashableをupper boundとする型変数を定義
```python
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の型チェックの仕様上は異なるようです。

### 辞書を変数に代入しない
```python
def hoge1(arg1: dict[Hashable, str]):
    pass

hoge1({ "1": "1" }) # エラー出ない
```

辞書を変数に代入せず直接関数に渡すと、型チェックでエラーは出ませんでした。
なぜでしょうか？理由は推測できますが、仕様がどうなっているのか私にはよく分かりませんでした。

これまで述べた通り、辞書はmutableなのでその変性はinvariant（非変）だから、エラーになります。それは辞書を変数に代入した場合の話です。
変数に代入してないリテラル時点ではimmutableだから多分covariant（共変）になり、`dict[str, str]`が`dict[Hashable, str]`のサブタイプとみなせるようになるからエラーは出ない、という事なのかなと思いましたが、Pythonの型チェックが実際本当にそういう仕様なのかは分かりません。


## 参考文献
- [\`str\` not detected as \`Hashable\` · Issue \#8293 · python/mypy](https://github.com/python/mypy/issues/8293)
- [Python の型ヒントを理解したい \- loglog\(\)](https://yng87.page/blog/2023/type-system-python/)
- [Pythonの型ヒントと共変性\(covariance\)と反変性\(contravariance\)、変性\(variance\) \- k\.dev](https://kdotdev.com/kdotdev/python-covariance-contravariance-variance)