はじめに
京都リサーチパークにて開催のYAPC::Kyotoでノベルティを受け取りし参加者の1人、nikkieです。
Pythonの型ヒントは進化が早く、Python3系のマイナーバージョンアップで型ヒントの新たな書き方が導入されることが多いと感じます。
Python 3.9と3.10について、Union関係で今の理解をアウトプットします。
目次
- はじめに
- 目次
- 動作環境
- まとめ
- オプショナル型の型ヒント
- Unionの別の書き方 (Python 3.10〜)
- Python 3.9でUnionの型ヒントに|を使いたい
- 今回のエントリのきっかけ(アンサーセクション)
- 終わりに
- (追記)P.S. 型ヒントまわりのオススメ記事
動作環境
以下の環境で試しています:
まとめ
- 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
(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
とすることで利用ができます。
-
きっかけとなった記事で送出されているのが
TypeError
なので、型チェッカの指摘ではなく、Python処理系による実行時エラーととらえました↩ - 「Optional[X] is equivalent to X | None (or Union[X, None]).」とドキュメントの記載も変わっています ref: https://docs.python.org/ja/3.10/library/typing.html#typing.Optional↩
- https://docs.python.org/ja/3/library/__future__.html#module-__future__↩
- このfuture文を追加したことで、型ヒントが遅延評価になるからという理解です↩