😺

Pythonの最新の型エイリアス(TypeAliasType)でisinstanceを使うとエラーになるよ

2025/01/12に公開

はじめに

Python3.12 から導入された新しい型エイリアスの定義方法に乗り換えてみたところ、isinstance関数の処理がエラーで落ちたのでその対処法を調べました。

忙しい人向け

__value__プロパティを使うとエラー回避できます。

type U = int | str
val = "hoge"
# これはエラー!
isinstance(val, U)  # TypeError
# これならエラーなく型の確認ができる!!
isinstance(val, U.__value__)  # True

Python の型エイリアス

Python では、TypeAliasを使うことで任意の型に別名(エイリアス)を付けることができます。

from typing import TypeAlias

# 型エイリアスの定義
T: TypeAlias = int | str

# 以下のように書いてもpythonが型エイリアスだと認識してくれるので同じ意味
T = int | str

ただし、紹介しておいていきなりですが、上記のTypeAliasを使った型エイリアスの定義は python3.12 から非推奨になっています。

参照:typing --- 型ヒントのサポート

バージョン 3.12 で非推奨: TypeAlias is deprecated in favor of the type statement, which creates instances of TypeAliasType and which natively supports forward references. Note that while TypeAlias and TypeAliasType serve similar purposes and have similar names, they are distinct and the latter is not the type of the former. Removal of TypeAlias is not currently planned, but users are encouraged to migrate to type statements.

上記の引用文のとおり、python3.12 から推奨されている型エイリアスの定義方法が以下のようなtypeを使った定義方法です。

# python3.12から推奨される型エイリアスの定義
type T = int | str

書き方だけをぱっと見ても、新しい記法の方が型を定義しているのが分かりやすくて良さそうです。
(あと個人的には TypeScript の型定義の構文と似ているのも気に入った)

新しい型エイリアスで isinstance による型の確認はできません

タイトルの件ですが、
型の一致を確認するisinstance関数の第二引数に対して、最新の型エイリアスを使うとエラーが発生します。

from typing import TypeAlias

OldAlias: TypeAlias = str | int
type NewAlias = str | int

val = "hoge"

# 古い型エイリアスではisinstanceを使ってもエラーにならない
isinstance(val, OldAlias)

# 新しい型エイリアスではisinstanceを使うとエラーになる
isinstance(val, NewAlias)  # TypeError

なぜ??

上記の事象を理解するには、まず型エイリアスの新旧の定義方法で何が異なるのかを理解する必要があります。

まず、古い方法(TypeAliasを使った方法)では型エイリアスは紐づけた型に応じた型オブジェクトになります。

from typing import TypeAlias

H: TypeAlias = str
print(type(H)) # <class 'type'>

J: TypeAlias = int | str
print(type(J)) # <class 'typing.UnionType'>

K: TypeAlias = list[int]
print(type(K))  # <class 'types.GenericAlias'>

一方、新しい方法(typeを使った方法)では型エイリアスはTypeAliasTypeというオブジェクトになります。

type H = str
print(type(H)) # <class 'typing.TypeAliasType'>

type J = int | str
print(type(J)) # <class 'typing.TypeAliasType'>

type K = list[int]
print(type(K))  # <class 'typing.TypeAliasType'>

つまり、TypeAliasTypeを使って定義された型エイリアスは型オブジェクトではないため、isinstance関数の第二引数には渡せないという訳です!

ちなみに、TypeAliasType自体は型オブジェクトなのでisinstance関数の第二引数に渡すことができます。

from typing import TypeAliasType

type J = int | str
# これはエラーにならない
isinstance(J, TypeAliasType)  # True
# TypeAliasType自体は型オブジェクト
print(type(TypeAliasType)) # <class 'type'>

理屈はわかった、でもなんでこのような仕様になってるの?

上記のようなTypeAliasTypeの仕様はPEP 695で策定されたものです。

ただ、この仕様に困惑?している人は他にもいるようで、python のコミュニティフォーラムでも既に議論がなされていました。

参考:Run-time behaviour of `TypeAliasType

上記を読んでいくと、
あるユーザーからの投稿でTypeAliasTypeによる型エイリアスもisinstanceでサポートされるようにしたい、という声がありましたが、
CPython の Core Developer がそれはすべきでは無い、と意見を述べています。(参考

その理由としては、TypeAliasTypeによる型エイリアスは以前のものと比べ以下のような機能的な違いがあるためだそうです。

  • 型パラメータの明確なスコープの提供
    • 古い記法と比べ、type SomeGeneric[T] = ...のようなジェネリクス型が含まれた場合にスコープが明確になるそうです。(正直あまり理解できていない。。。)
  • 遅延評価
    • type X = <something>が実行された時点では<something>の部分は評価されず、<something>の部分が明示的に必要になった場合に評価されるそうです。
    • このような機構をもつことで、再帰型の実装などがより簡単になるそうです。

これらの機能とisinstance関数との相性が悪く、TypeAliasTypeによる型エイリアスがisinstance関数でサポートされない理由になっているようです。

対応方法は無いの??

一番簡単な対応方法としては、TypeAliasTypeを使わずに古い記法のTypeAliasを使うことです。
ただ、TypeAliasは python3.12 から非推奨になっているのであまり気乗りする方法では無いかもしれません。

実は、もう1つ対応方法があり、それはTypeAliasTypeオブジェクトの__value__プロパティを呼ぶ方法です。(参考

上記で説明したとおり、TypeAliasTypeは通常遅延評価されますが、__value__プロパティを呼ぶことで評価が即時実行され型オブジェクトが取得できる、という訳です。

type U = int | str
val = "hoge"
# これならエラーなく型の確認ができる!!
isinstance(val, U.__value__)  # True

ただ、見た目からも分かる通り若干TypeAliasTypeのハック的な使い方になる点と、
ジェネリクス型と組み合わせた際に__value__を使った方法でもうまくいかないケースがあるようなので、注意が必要です。

さいごに

TypeAliasTypeを使った型エイリアスでisinstance関数を使う方法について説明しました。

最近の Python ではかなり型のサポートが充実してきましたが、この記事のような一見わかりにくい挙動もまだまだあるなと感じました。

自分自身、型など何も意識せずに 5 年くらいコードを書いてきた身ですが、
TypeScript を使い始め型の魅力に取り憑かれてからは、 Python でも型が無いと不安になる体になってしまいました。
(じゃあ Python じゃなくて最初から型がある言語を使えって??、、、、、うん、それはそう。)

世の中の Python 型絶対つけるマンの方々にとってこの記事が少しでも参考になれば幸いです。

Discussion