2024年11月: 型ヒントの動向と新しい機能の紹介¶
鈴木たかのり(@takanory)です。 今月の「Python Monthly Topics」では、Pythonの型ヒントの最近の動き、比較的新しい型ヒントの機能について紹介します。
本連載でも過去にいくつも型ヒント関連の記事があります。 このようによりよいPythonコードを書くための型ヒントが、Pythonバージョンの更新に伴って追加されています。
- 2023年1月:O/Rマッパーの型チェックを強化できるPython 3.11の新機能 Data Class Transforms(PEP 681) | gihyo.jp 
- 2023年5月:Python 3.11の新機能:型チェッカーでロジックの間違いを検出できるtyping.assert_never関数とtyping.Never型 | gihyo.jp 
- 2023年9月:Python 3.12の新機能「PEP 692: Using TypedDict for more precise **kwargs typing」の紹介 | gihyo.jp 
PEP 729 – Typing governance process:型ヒントのガバナンス¶
今回調べて知ったんですが、以下のドキュメント(PEP 729)で型ヒントのガバナンス(運営管理)方法について提案されており、2023年11月に採択されました。
PEP 729では型ヒントの保守、開発を行うPython型ヒント評議会(Python Typing Council)を立ち上げることが提案されています。 Pythonの言語仕様はPEP(Python Enahncement Proposal:Python拡張提案)というドキュメントで提案され、その提案が採択されることで決定されます。 PEPを採択するかの判断はPython Steering Council(以下SC)が行っており、SCは5名の評議委員で構成されています。 しかし、SCはPythonのすべてのPEPに対して検討・判断を行うため「型ヒントのPEPに対して適切な判断をすることが難しい」ということがPEP 729で主張されています。
最初に書いたとおりPEP 729はSCによって2023年11月に採択され、現在5名の初期メンバーがSCにより任命されています。 5名のメンバーは、以下のように型ヒントや型関連のツールに習熟した開発者が選ばれています。
現在のメンバーは以下のリポジトリで記録されています。 メンバー数は3〜5名で最長連続5年などの制限があります。
評議会はその取り組みの一部として、以下のこと行います。
- 仕様に準拠したテストスイートの作成 
- 型システムの仕様の作成 
- 型システムのユーザー向けリファレンスの作成 
なお、Python Steering Councilでの運営は、Guido van Rossum氏がBDFLから引退したことを受け、2019年から始まっています。 SCについて詳細を知りたい方は以下の参考資料を確認してください。
型ヒントのドキュメント¶
上記の評議会の成果の一つとして、型ヒント、型システムを使用するユーザー向けのリファレンスドキュメントが以下のサイト「Static Typing with Python」で公開されています。
 
Static Typing with Python¶
いままで型ヒントについてはPEPのみが仕様書で、PEPドキュメント自体は仕様を議論するためのドキュメントのため、ユーザー用のドキュメントとしては適切ではありません。 そこで、型システムについてのユーザー向けドキュメントが作成されました。 このドキュメントでは型システムのガイド、リファレンス、仕様が書かれています。 また、型関連ツールが紹介されています。
仕様(Specification)のセクションには、各種型ヒントの仕様や使い方が記載されています。 以降で説明する新しい型ヒントの機能についても、元となったPEPのドキュメントから、型ヒントのドキュメントへの参照が追加されています。
 
「Attention」の中で型ヒントのドキュメントを参照している¶
型ヒントの公式ドキュメントは以下のリポジトリで管理されています。
@override デコレーター¶
- Python公式ドキュメント:@typing.override 
- 型ヒントドキュメント:@override 
- PEP:PEP 698 – Override Decorator for Static Typing | peps.python.org 
Python 3.12でtypingモジュールに@overrideというデコレーターが追加されました。
このデコレーターはクラスを継承したときに、サブクラスがスーパークラスの属性やメソッドをオーバーライドしていることを表します。 もし、このデコレーターが付いている属性やメソッドが、実際にはなにもオーバーライドしていない場合は、型チェッカーはエラーを出力します。 こうすることで「オーバーライドしたつもりだけど、実はオーバーライドしていない」というミスを防ぎます。
以下のサンプルコードではPetを継承したFerretサブクラスを作成しています。
サブクラスでは2つのメソッドを定義して、両方に@overrideデコレーターを指定しています。
from typing import override
class Pet:
    """ペットを表すクラス"""
    name: str
    def sleep(self) -> None:
        print(f"{self.name}-chan is sleeping.")
    def eat(self) -> None:
        print(f"{self.name}-chan is eating.")
class Ferret(Pet):
    """フェレットを表すクラス"""
    @override
    def sleep(self) -> None:  # OK
        print(f"Ferret, {self.name}-chan is sleeping.")
    @override
    def eet(self) -> None:  # NG
        print(f"Ferret, {self.name}-chan is eating.")
pyrightでこのコードをチェックすると「eetメソッドにoverrideマークが付いているが、ベースメソッドが存在していない」というエラーが発生します。
% pyright override_sample.py 
/.../override_sample.py
  /.../override_sample.py:22:9 - error: Method "eet" is marked as override, but no base method of same name is present (reportGeneralTypeIssues)
1 error, 0 warnings, 0 informations 
このエラーは、サブクラスで本来eatと書くべき所をeetと間違えているために発生しています。
この間違いに気がつかないとFerret.eat()を呼び出した時に、親クラスのメソッドが呼び出されてしまいます。
@overrideを書くことによってpyright、mypyなどの型チェッカーでチェックできるようになります。
TypedDictでRequiredとNotRequiredを使用する¶
- Python公式ドキュメント:typing.Required 
- 型ヒントドキュメント:Required and NotRequired 
- PEP:PEP 655 – Marking individual TypedDict items as required or potentially-missing | peps.python.org/ 
Python 3.11でtypingモジュールにRequiredとNotRequiredが追加されました。
この2つの型ヒントは、TypedDictと組み合わせ使用します。
TypedDictで辞書の各キーに対して型ヒントを定義し、各キーに対して必須、非必須を指定できます。
TypedDictの基本¶
まずはTypedDictの動作を確認します。
以下のコードでは辞書の3つのキー(name、farm、age)に対して、型ヒントでstrやintを指定しています。
from typing import TypedDict
class Ferret(TypedDict):
    """フェレットを表す辞書"""
    name: str
    farm: str
    age: int
guri1: Ferret = {"name": "guri", "farm": "Canadian", "age": 5}  # OK
guri2: Ferret = {"name": "guri", "farm": "Canadian", "age": "five"}  # NG
guri3: Ferret = {"name": "guri", "age": 5}  # NG
このコードをpyrightでチェックすると、2番目と3番目のパターンでエラーが発生します。
12行目はageにはintを指定すべきところをstrを指定しているためにエラーになっています。
13行目はfarmキーが指定されていないためにエラーとなっています。
% pyright typeddict_sample1.py 
/.../typeddict_sample1.py
  /.../typeddict_sample1.py:12:61 - error: Type "dict[str, str]" is not assignable to declared type "Ferret"
    "Literal['five']" is not assignable to "int" (reportAssignmentType)
  /.../typeddict_sample1.py:13:17 - error: Type "dict[str, str | int]" is not assignable to declared type "Ferret"
    "farm" is required in "Ferret" (reportAssignmentType)
2 errors, 0 warnings, 0 informations 
NotRequiredを使用¶
farmキーを非必須にするためにNotRequiredを使用します。
型ヒントにNotRequired[str]と書くことで、このキーが非必須(オプション)となります。
from typing import NotRequired, TypedDict
class Ferret(TypedDict):
    """フェレットを表す辞書"""
    name: str
    farm: NotRequired[str]  # farmを非必須にする
    age: int
gura1: Ferret = {"name": "gura", "farm": "Path Valley", "age": 6}  # OK
gura2: Ferret = {"name": "gura", "farm": "Path Valley", "age": "six"}  # NG
gura3: Ferret = {"name": "gura", "age": 6}  # OK
このコードをpyrightでチェックすると、farmキーを指定していない13行目がエラーではなくなります。
% pyright typeddict_sample2.py 
/.../typeddict_sample2.py
  /.../typeddict_sample2.py:12:64 - error: Type "dict[str, str]" is not assignable to declared type "Ferret"
    "Literal['six']" is not assignable to "int" (reportAssignmentType)
1 error, 0 warnings, 0 informations 
Requiredを使用¶
TypedDictでtotal=Falseと指定するとすべてのキーが非必須となります。
その場合、逆にRequiredを使用して必須に指定できます。
以下のコード例ではnameとageのみを必須としています。
from typing import Required, TypedDict
class Ferret(TypedDict, total=False):
    """フェレットを表す辞書"""
    name: Required[str]
    farm: str
    color: str
    age: Required[int]
seven1: Ferret = {"name": "seven", "farm": "Far Farm", "age": 6}  # OK
seven2: Ferret = {"name": "seven", "color": "Black Self", "age": 6}  # OK
seven3: Ferret = {"name": "seven", "age": 6}  # OK
seven3: Ferret = {"name": "seven", "color": "Black Self"}  # NG
上記のコードをpyrightでチェックすると、最後のパターンで必須(Required)の要素ageが指定されていないためエラーとなります。
% pyright typeddict_sample3.py   
/.../typeddict_sample3.py
  /.../typeddict_sample3.py:15:18 - error: Type "dict[str, str]" is not assignable to declared type "Ferret"
    "age" is required in "Ferret" (reportAssignmentType)
1 error, 0 warnings, 0 informations 
任意の文字列リテラル型¶
- Python公式ドキュメント:typing.LiteralString 
- 型ヒントドキュメント:LiteralString 
- PEP:PEP 675 – Arbitrary Literal String Type | peps.python.org 
Python 3.11でtypingモジュールにLiteralStringが追加されました。
この型ヒントは文字列リテラルのみを表します。
str型ではなく、文字列リテラルで作成した文字列のみで構成されたや文字列のみが使用できます。
なおLiteralStringは型チェックにのみに使用される特別な形式でデータ型としては存在しません。
from typing import LiteralString
from pathlib import Path
def eat(name: LiteralString) -> None:
    """LiteralString型のnameのみを受け取る"""
    print(f"{name}-chan is eating.")
eat("seven")  # OK
name = "seven"
eat(name)  # OK
eat(name.title())  # OK
eat("Ferret, " + name)  # OK
name = input()
eat(name)  # NG
eat(Path("name.txt").read_text())  # NG
上記のコードをpyrightでチェックすると、input()で入力を受け取る場合とファイルから文字列を取得する場合にLiteralStringではなくstrとなるためエラーが発生します。
.title()で文字列を変換する場合や+演算子で文字列を連結していても、元となる文字列がLiteralString型の場合は変換された文字列もLiteralStringとなるためエラーとなりません。
% pyright literalstring_sample.py
/..;/literalstring_sample.py
  /.../literalstring_sample.py:16:5 - error: Argument of type "str" cannot be assigned to parameter "name" of type "LiteralString" in function "eat"
    "str" is not assignable to "LiteralString" (reportArgumentType)
  /.../literalstring_sample.py:17:5 - error: Argument of type "str" cannot be assigned to parameter "name" of type "LiteralString" in function "eat"
    "str" is not assignable to "LiteralString" (reportArgumentType)
2 errors, 0 warnings, 0 informations 
この機能は、たとえばプログラム中で実行するSQLやシェルのコマンドに、ユーザーが入力したデータが混入することを防ぐといった用途に使用できます。
以下のコード例では、最後の2つのパターンはLiteralStringではないため、型チェックを実行するとエラーとなります。
from typing import LiteralString
def run_query(sql: LiteralString) -> None:
    ...
def caller(arbitrary: str, literal: LiteralString) -> None:
    run_query("SELECT * FROM animals")  # OK
    run_query(literal)  # OK
    run_query("SELECT * FROM " + literal)  # OK
    run_query(f"SELECT * FROM {literal}")  # OK
    run_query(arbitrary)  # NG
    run_query(f"SELECT * FROM animals WHERE name = {arbitrary}")  # NG
まとめ¶
本記事では、最近の型ヒントの動きとして以下を紹介しました。
- PEP 729 – Typing governance processが採択され「Python型ヒント評議会」が発足したこと 
- 型ヒントのドキュメント「Static Typing with Python」がhttps://typing.readthedocs.io/で公開されていること 
また、最近追加された以下の型ヒントを紹介しました。
- オーバーライドしたメソッドに指定する - @overrideデコレーター
- TypedDictの必須、非必須のキーを指定する- Required、- NotRequired
- 文字列リテラルのみを含む - LiteralString
評議会による運営により、今後も継続的に型システムに関する機能が追加・改善していくと思われます。 どのような機能が出てくるのか楽しみです。