PEP 561 に準拠した型ヒントを含むパッケージの作り方

この記事では Python 3.7 で採択された PEP 561 Distributing and Packaging Type Information について簡単に解説し,PEP 561 に準拠した,型ヒントを含むパッケージの作成方法について紹介します.

PEP 561 以前の型情報の配布事情

PEP 561 以前は主に typeshed を通じて,標準ライブラリ・サードパーティーの型情報が配布されていました.typeshed にはパッケージの開発者が同意したパッケージの型情報しか登録できないという制限や,どのバージョンのパッケージの型情報かという情報が含まれていないという問題,typeshed のメンテナーによるレビューが必要なためスケールしづらいといった問題がありました.

詳細は割愛しますが,PEP 484 の “Storing and distributing stub files” の節にもスタブファイル (型情報) の配布方法が書かれていました.しかし,あまり使いやすい/分かりやすいものとは言えず,現在では PEP 561 の方法が推奨されています.

また,mypy では MYPYPATH を使って,スタブファイルを置いたディレクトリを指定して型検査を行うことが出来ます.しかし,これは PEP 484 で定められた範囲外の mypy 独自の機能でした.

PEP 561 の簡単な解説

PEP 561 では主に次のことが定められています.

  • 型ヒントがついたパッケージを作成・配布する方法 (インラインパッケージ)
  • 他のパッケージのための,型ヒントのみを含むパッケージを作成・配布する方法 (スタブパッケージ)
  • Type Checker Module Resolution Order (詳細は後述)

これらが定められたことで,以下のようなことが可能になりました.

  • typeshed を利用せずに,自分が開発したパッケージを型ヒント付きで配布できる
  • 型ヒントのスタブファイルのみを含むパッケージを配布できる
    • 使用例1: 型ヒントをパッケージ本体と一緒に配布したくない場合
    • 使用例2: パッケージ開発者と別の人が型ヒントを作成して配布したい場合
    • 使用例3: プライベート (社内など) に,型情報を配布したい場合

具体的な例としては numpy の型ヒントが numpy-stubs というパッケージとして開発されています.

Type Checker Module Resolution Order

Type Checker Module Resolution Order は型検査器がどの順序で型情報 (スタブファイル) を参照するべきかのルールです.具体的には次のように定義されています.(上のものの方が優先順位が高い)

  1. ユーザーの書いているコード (型検査器が実行されるコード)
  2. 手動で設定されたスタブファイルや Python のソース (MYPYPATH など)
  3. スタブパッケージ (後述する -stubs でパッケージ名が終わる,スタブファイルのみを含むパッケージ)
  4. インラインパッケージ (型ヒントを含むパッケージ)
  5. typeshed

この規則が定義されたことで,typeshed で配布されている型情報に問題がある場合に,スタブパッケージを作成して型情報を上書きするといったことが可能になります.

PEP 561 への型検査器の対応状況

2018年4月リリースの mypy 0.590 から PEP 561 の実験的なサポートが追加されています.Google の pytypeFacebook の pyre-check でもそれぞれ PEP 561 対応の issue は作られていますが,まだ実装は進んでいないようです.

PEP 561 に準拠したパッケージの作り方

ここからは PEP 561 に準拠したパッケージの作り方をいくつかの3つのケースに分けて紹介します.ここで紹介しているサンプルはすべて GitHub の ymyzk/pep-561-samples で公開しています.この記事では say_hello という関数を一つだけ含む, hello というシンプルなパッケージを題材にします.

# hello/__init__.py
def say_hello(name):
    print(f"Hello, {name}")

型ヒント付きのパッケージを配布したい (アノテーション編)

次のようにアノテーションを用いて,型ヒントを記述しているパッケージを配布する方法を紹介します.(リポジトリの hello-typed1 に対応)

# hello/__init__.py
def say_hello(name: str) -> None:
    print(f"Hello, {name}")

この場合は py.typed という名前の空ファイルを型情報を含むパッケージの直下に置き,パッケージに含めます.パッケージのディレクトリ構成:

hello-typed1
├── hello
│   ├── __init__.py
│   └── py.typed
└── setup.py

setup.pypackage_data を用いて,py.typed がパッケージに含まれるようにします.

# setup.py
from setuptools import setup

setup(
    name="hello-typed1",
    version="0.0.1",
    packages=["hello"],
    package_data={
        "hello": ["py.typed"],
    },
)

簡単なまとめ: ファイル py.typed をパッケージのディレクトリに置く / package_datapy.typed を含める

型ヒント付きのパッケージを配布したい (スタブファイル編)

次のようにスタブファイル (拡張子が .pyi のファイル) を用いて,プログラム本体と別にスタブファイルに型ヒントを記述しているパッケージを配布する方法を紹介します.この方法は Python 2/3 両対応のライブラリを開発している場合に有用です. (リポジトリの hello-typed2 に対応)

# hello/__init__.py
def say_hello(name):
    print(f"Hello, {name}")
# hello/__init__.pyi
def say_hello(name: str) -> None: ...

この場合も py.typed という名前の空ファイルを型情報を含むパッケージの直下に置き,パッケージに含めます.パッケージのディレクトリ構成:

hello-typed2
├── hello
│   ├── __init__.py
│   ├── __init__.pyi
│   └── py.typed
└── setup.py

setup.pypackage_data を用いて,py.typed とスタブファイル (.pyi) がパッケージに含まれるようにします.

# setup.py
from setuptools import setup

setup(
    name="hello-typed2",
    version="0.0.1",
    packages=["hello"],
    package_data={
        "hello": ["py.typed", "*.pyi"],
    },
)

簡単なまとめ: ファイル py.typed をパッケージのディレクトリに置く / package_datapy.typed*.pyi を含める

他のパッケージのための型ヒントを別のパッケージとして配布したい

先ほど挙げた numpy-stubs のようなパッケージを配布する方法を紹介します.ここでは,型ヒントが付いていないパッケージの hello のためのスタブファイルのみを含む hello-stubs というパッケージを作成します. (リポジトリの hello / hello-stubs に対応)

# hello-stubs/__init__.pyi
def say_hello(name: str) -> None: ...

この場合はオリジナルのパッケージ名の末尾に -stubs というサフィックスを付けます.また, -stubs からこのパッケージが型情報を含んでいることは明らかであるとの理由から py.typed を置く必要はありません.パッケージのディレクトリ構成:

hello-stubs
├── hello-stubs
│   ├── __init__.pyi
└── setup.py

setup.pypackage_data を用いて,スタブファイル (.pyi) がパッケージに含まれるようにします.また,install_requires を用いて,オリジナルのパッケージのどのバージョンのためのスタブファイルを含むパッケージかを明示することが推奨されています.

# setup.py
from setuptools import setup

setup(
    name="hello-stubs",
    version="0.0.1",
    packages=["hello-stubs"],
    package_data={
        "hello-stubs": ["*.pyi"],
    },
    install_requires=[
        "hello==0.0.1",
    ],
)

簡単なまとめ: オリジナルのパッケージ名の後ろに -stubs を付けてスタブパッケージを作る / package_data で *.pyi を含める / install_requires でオリジナルのパッケージを指定する.(追記: 2018/9/30 この段落の package_data について,誤りがあったため修正しました.2018/11/4 この段落の setup.py の内容の誤りを修正しました.)

まとめ

この記事では PEP 561 Distributing and Packaging Type Information について簡単に紹介した上で,PEP 561 に準拠したパッケージの作成方法 (py.typed など) を具体例を交えて紹介しました.Partial stub package など一部説明していない部分もあるので,詳細は PEP 561 を参照して下さい.

また,現時点で自分が調べた範囲で分かっていないことが二つあります.一つは typeshed についてで,今後サードパーティーのパッケージの型情報の配布が typeshed から PEP 561 を用いてパッケージに型情報を含める方法が主流になるのかが不明です.もう一つはスタブパッケージの探し方についてです.現時点では PyPI に登録されているスタブパッケージの一覧を簡単に得る方法はないように思います.もしスタブパッケージのための trove classifier が定義されれば,スタブパッケージが探しやすくなるかもしれません.