🤔

Python の typing.Union における型順序の上書き現象

に公開

Python の typing.Union における型順序の上書き現象

huggingface/transformers の PR 39467Union[str, dict]Union[dict, str] の順序の違いでエラーが発生していたというのを見かけた。そんなことがあり得るのかと驚き、実際に確認してみることにした。

調べてみると、Python の typing.Union には特定の条件下で型の順序が上書きされるという興味深い挙動があることが分かった。なかなかにややこしい挙動であり、将来この現象について思い出せるよう、その詳細をここに残しておく。

なお、確認には Python 3.12.11 を使用した。

基本的な現象

Optional[Union[A, B]] の形で型を定義すると、後から定義した型の順序が最初に定義した型の順序で上書きされる。

from typing import Union, Optional

type1 = Optional[Union[str, dict]]
type2 = Optional[Union[dict, str]]
print(type1)
print(type2)

実行結果:

# typing.Union[str, dict, NoneType]
# typing.Union[str, dict, NoneType] <-- 本来は dict, str の順序のはずが上書きされている

条件の詳細確認

この現象は Optional を使用しない場合には発生しない。

from typing import Union

type1 = Union[str, dict]
type2 = Union[dict, str]
print(type1)
print(type2)

実行結果:

# typing.Union[str, dict]
# typing.Union[dict, str]  <-- 期待通り順序が保たれている

興味深いことに、typing.Dictdict のように本質的には同じ型でも、表記が異なる場合は上書きされない。

from typing import Union, Optional, Dict

type1 = Optional[Union[str, Dict]]
type2 = Optional[Union[dict, str]]
print(type1)
print(type2)

実行結果:

# typing.Union[str, typing.Dict, NoneType]
# typing.Union[dict, str, NoneType]  <-- 上書きされない

また、型の数が3つ以上の場合でも同様の現象が発生する。

from typing import Union, Optional

type1 = Optional[Union[str, int, dict]]
type2 = Optional[Union[dict, str, int]]
print(type1)
print(type2)

実行結果:

# typing.Union[str, int, dict, NoneType]
# typing.Union[str, int, dict, NoneType] <-- 上書きされている

原因についての考察

この現象の正確な原因は現時点では分からない。型情報がキャッシュされているのかなと思ったが、その場合は Optional を使用しない場合でも上書きが発生するはずであり、実際の挙動と一致しないと思われる。

感想

この挙動がエラーを引き起こすケースは稀だと思われるが、transformers のような有名なライブラリでも実際に問題となっていることは興味深い。Type hints を扱う際には、このような予期しない挙動があることを念頭に置いておくといつか役に立つかもしれない。

Discussion