nikkie-ftnextの日記

イベントレポートや読書メモを発信

PythonのUnionまわりの型ヒントの書き方を整理する(Python 3.9と3.10が|が使える境目です)

はじめに

京都リサーチパークにて開催のYAPC::Kyotoでノベルティを受け取りし参加者の1人、nikkieです。

Pythonの型ヒントは進化が早く、Python3系のマイナーバージョンアップで型ヒントの新たな書き方が導入されることが多いと感じます。
Python 3.9と3.10について、Union関係で今の理解をアウトプットします。

目次

動作環境

以下の環境で試しています:

  • Python 3.9.16
  • Python 3.10.9
  • mypy 1.1.1
    • Pythonのバージョンごとに仮想環境を作り、インストールしました

まとめ

  • Python 3.9でも3.10でも Optional[X]Union[X, None] であることは変わりません
  • Union|で書けるようになったのが、Python 3.10から
    • つまり、Python 3.9では|で書けません
  • Python 3.9で型ヒントのUnion|で書くにはfrom __future__ import annotationsしましょう

以下のコードはPython 3.9でも3.10でも動きます。

from __future__ import annotations

from typing import Optional, Union


def test(
    OK: Optional[str],
    OK2: Union[str, None],
    OK3: str | None,
):
    pass

オプショナル型の型ヒント

Python 3.9のドキュメントを引きます。
https://docs.python.org/ja/3.9/library/typing.html#typing.Optional

Optional[X]Union[X, None] と同値です。

なので、以下のようにOptionalをNoneとのUnionとも書けます

from typing import Optional, Union


def test(
    OK: Optional[str],
    OK2: Union[str, None],
):
    pass

このスクリプトを実行してみましょう1

(venv39) % python optional_annotation.py
(venv39) % 

実行時にエラーは送出されません。

型チェックも通ります。

(venv39) % mypy optional_annotation.py
Success: no issues found in 1 source file

Optional[X]Union[X, None] と書けるのです!
(確認を省略しますが、Python 3.10でもエラーは送出されませんし、型チェックも通ります)

Unionの別の書き方 (Python 3.10〜)

PEP 604 – Allow writing union types as X | Y | peps.python.orgで提案され、Python 3.10からType Union Operator |が導入されました🎉
https://docs.python.org/ja/3/whatsnew/3.10.html#pep-604-new-type-union-operator

Union[X, Y]X | Yと書けます!
「What's New In Python 3.10」にある書き換え例です。

def square(number: Union[int, float]) -> Union[int, float]:
    return number ** 2
def square(number: int | float) -> int | float:
    return number ** 2

OptionalはNoneとのUnionですから、Optional[X]X | None と書けちゃうわけです!2
短く済んで最高ですね。

Python 3.10で動作確認しましょう。

from typing import Optional, Union


def test(
    OK: Optional[str],
    OK2: Union[str, None],
    OK3: str | None,
):
    pass
(venv310) % python optional_annotation.py
(venv310) % 
(venv310) % mypy optional_annotation.py
Success: no issues found in 1 source file

Unionを|で書き換えられることで、OptionalもNoneとのUnion(|)として書けました。

Python 3.9でUnionの型ヒントに|を使いたい

Python 3.10で動作したコードですが、これは3.9では動きません。

(venv39) % python optional_annotation.py
Traceback (most recent call last):
  File "/.../optional_annotation.py", line 7, in <module>
    OK3: str | None,
TypeError: unsupported operand type(s) for |: 'type' and 'NoneType'

型チェックでは以下のようになります

(venv39) % mypy optional_annotation.py
optional_annotation.py:9: error: X | Y syntax for unions requires Python 3.10  [syntax]
Found 1 error in 1 file (checked 1 source file)

これは、Python 3.9では|演算子がUnion型として使えるように拡張されていないことによります。
Python 3.10からは|が使えるのに、3.9ではtyping.Unionを使わざるを得ないのはちょっと残念ですよね。
「短く済む|を使っていきたいけど、サポートするPythonのバージョンを3.10以上に上げるまではtyping.Unionを使わざるを得ないのかな」と過去の私は考えていました。

ですが、Python 3.9でもUnionの代わりに|を使えるんです!
それを可能にするのが1行目のfuture文3

+from __future__ import annotations

from typing import Optional, Union


# 以降は共通なので省略します

これを入れただけで実行時のエラーは出ません。

(venv39) % python optional_annotation.py
(venv39) % 

mypyも通ります!

(venv39) % mypy optional_annotation.py
Success: no issues found in 1 source file

これを知ったのは、以下の記事のおかげです。

Python 3.7から3.9であれば、from __future__ import annotationsを記述すればこの記法が使えるようになります。

今回のエントリのきっかけ(アンサーセクション)

コメントとして書こうかと思ったのですが、ドキュメントなど参照しながら書いていたら長くなったので、アンサーブログ形式としました。

SQLAlchemy 2.0.7を両環境にインストールしています。

from typing import Optional, Union

from sqlalchemy import and_


def test(
    OK: Optional[and_],
    OK2: Union[and_, None],
    OK3: and_ | None,
):
    pass

このスクリプトPython 3.9でも3.10でもエラーを送出します。
エラーの内容は共通だったので、3.10の方を見ていきます

(venv310) % python optional_annotation.py
Traceback (most recent call last):
  File "/.../optional_annotation.py", line 9, in <module>
    OK3: and_ | None,
TypeError: unsupported operand type(s) for |: 'function' and 'NoneType'

Python 3.10ではUnion型として|が使えるはずですが、なぜエラーが送出されるのでしょう。
エラーメッセージに注目すると、sqlalchemy.and_というfunction(関数)を型ヒントに使っているのがエラーの原因のようです。

|演算子typeオブジェクト同士、またはtypeオブジェクトとNoneについての演算がサポートされています。

>>> str | None
str | None
>>> type(str)
<class 'type'>

上のスクリプトで型ヒントに使ったand_ですが、これはtypeオブジェクトではなく、functionオブジェクトです。
typeオブジェクトでないので、|TypeErrorを送出しました。

>>> from sqlalchemy import and_
>>> type(and_)
<class 'function'>
>>> and_ | None
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for |: 'function' and 'NoneType'

typing.Optionalを使うとき、|と違ってOptionalに渡るオブジェクトがどんなオブジェクトかは検証しないようで、実行時にエラーは送出されません。

>>> from typing import Optional
>>> Optional[and_]
typing.Optional[and_]

Python処理系の実行時のエラーは出ませんが、mypyの型チェックは通りませんでした。
なのでand_というfunctionオブジェクトを型ヒントには使うべきではないというのが私の結論です。

スクリプトの7行目はOK: Optional[and_],という型ヒントの箇所のチェック結果ですが、

optional_annotation.py:7: error: Function "sqlalchemy.sql._elements_constructors.and_" is not valid as a type  [valid-type]
optional_annotation.py:7: note: Perhaps you need "Callable[...]" or a callback protocol?
  • sqlalchemy.and_が型として有効(valid)ではないこと
  • typing.Callableまたはコールバックプロトコルを使って書き直す必要があること

がmypyから案内されています(OK2, OK3についても共通なので省略します)

typing.Callableを使って書き直した例が以下です。
https://docs.python.org/ja/3.10/library/typing.html#typing.Callable

sqlalchemy.and_シグネチャを確認して、型ヒントに使うための型(AndCallable)を宣言しました。
https://github.com/sqlalchemy/sqlalchemy/blob/rel_2_0_7/lib/sqlalchemy/sql/_elements_constructors.py#L116-L119

これでPython処理系も型チェックもどちらも通っています。

(venv310) % mypy optional_annotation.py
Success: no issues found in 1 source file
(venv310) % python optional_annotation.py
(venv310) %

アンサーセクション冒頭で示したコードにfrom __future__ import annotationsを追加しても、Python処理系はエラーを送出しなくなります4
ですが、型チェックは通りませんand_について同じ案内がされます)。
なので、型ヒントに使えないオブジェクトを型ヒントに使っているという型ヒントの誤りを修正する必要があると考えます。

終わりに

Python 3.10からUnionを|でも書けるようになったまわりの知識の整理のアウトプットでした、

|によるUnionとtyping.Unionとでオブジェクトの型をチェックするかどうかが違うというのは発見でした。
型ヒントとしては同じはずですが、検証しているかどうか動きが違うというのは、ユーザの誤解を招きうるので"バグ"なのかもしれません。
Issueなど見れたら覗いてみよう。

(追記)P.S. 型ヒントまわりのオススメ記事

当時は情報が限られていたのでフューチャー技術ブログを見つけられていなかったら、from __future__ import annotationsを私が知っていたかは怪しいのですが、いまは情報が増えています!
Python Monthly Topicsの型ヒントの回がオススメです。

先ほど、dict型の値の宣言で用いたtyping.Unionですが、Python 3.10以降ではパイプ(|)を用いて記述することができます。

また、typing.Optonalで宣言していた、Noneまたはそれ以外の場合においてもパイプ(|)を用いることができます。(※ママ)

この機能をPython 3.7からPython 3.9までで利用したい場合は、from __future__ import annotationsとすることで利用ができます。


  1. きっかけとなった記事で送出されているのがTypeErrorなので、型チェッカの指摘ではなく、Python処理系による実行時エラーととらえました
  2. Optional[X] is equivalent to X | None (or Union[X, None]).」とドキュメントの記載も変わっています ref: https://docs.python.org/ja/3.10/library/typing.html#typing.Optional
  3. https://docs.python.org/ja/3/library/__future__.html#module-__future__
  4. このfuture文を追加したことで、型ヒントが遅延評価になるからという理解です