Python用のコードリンターであるmypyは,変数の型指定を厳格に行うためのlinterである.Pythonは本来動的型付け言語なのでコード自体は型付けなしでかけてしまうが,長期的なメンテナンスや複数人での開発を考えるとバグの温床になる.昔かいたコードをみて入力はどうするべきだっけとなるのはよくある話かと思う... そこで,静的型チェックを導入して擬似的に型指定を強制する方法が取られる.mypy はそのためのリンターである.

型指定を行ったPythonコードは,変数にコロンで変数を指定する.別途typingという型付け用のライブラリもあって柔軟な設定もできる.今回は型付け方法には深入りせず,これは既知のものとして話を進める.

"""sample.py"""
from typing import List

def greet(name: str) -> str:
    """基本的な型指定"""
    return "Hello, " + name

def average(values: List[float]) -> float:
   """typingを用いた型指定"""
    return sum(values) / len(values)

1. mypyとは何か

mypy はPython 3系で導入された型ヒント(type hints)を基盤として,コードの実行前に型の整合性を検査する.コード中に型の不一致があればそれを検出する.

mypyのメリット

  • バグの早期発見:実行前に型の不一致を発見できる
  • 可読性と保守性の向上:引数や戻り値の意図が明確になり、他者がコードを理解しやすくなる
  • 補完精度の向上:IDEやLSPとの連携で、補完やリファクタリングがより安全になる

2. mypyの基本的な使い方

インストールはpipまたはcondaからできる.

pip install mypy
conda install mypy

型チェック対象のサンプルコード

# sample.py
def greet(name: str) -> str:
    return "Hello, " + name

greet(42)  # 実行時はエラーにならないが、型的には入力をstr型にするべきで不正
greet("Taro") # こちらは型的にも正しい

型チェックの実行

$mypy sample.py
sample.py:5: error: Argument 1 to "greet" has incompatible type "int"; expected "str"
Found 1 error in 1 file (checked 1 source file)

このように,型ヒントに基づき実行前に型の不整合を検出できる.プロジェクトでは,github actionや,pre-commitでmypyによるチェックを強制することで,型指定がおかしいコードがコミット/マージされるのを防ぐような使い方をする.

また,numpyなどの外部ライブラリから利用している部分についても,その外部ライブラリに型指定があれば(大抵の有名どころはある)出力に反映される.一部のライブラリは別途型指定用のライブラリをインストールする必要がある.一例としてpandas では,pandas-stubs をインストールするとmypyが動作する.

pip install pandas-stubs

以下のようなpandasを利用したサンプルコードを準備する.

import pandas as pd

decimals = pd.DataFrame({'TSLA': 2, 'AMZN': 1})
prices = pd.DataFrame(data={'date': ['2021-08-13', '2021-08-07', '2021-08-21'],
                            'TSLA': [720.13, 716.22, 731.22], 'AMZN': [3316.50, 3200.50, 3100.23]})
rounded_prices = prices.round(decimals=decimals)

このコードはdecimalsの指定をpd.DataFrameでやっているところがおかしく,本来はSeriesを利用するべきである.pandas-stubs導入後にmypyを実行すると,以下のようにちゃんと警告が出る.

$mypy round.py
round.py:6: error: Argument "decimals" to "round" of "DataFrame" has incompatible type "DataFrame"; expected "Union[int, Dict[Any, Any], Series[Any]]"  [arg-type]
Found 1 error in 1 file (checked 1 source file)

3. 自作ライブラリにmypyを導入する方法

自作ライブラリに対してmypyによる型チェックを有効にするには、型付きパッケージのマークのための空ファイル(py.typed),およびプロジェクトの設定ファイル(setup.pyなど)に追加の設定が必要である。

3.1 ディレクトリ構成例

myproject/
├── mylib/
│   ├── __init__.py
│   ├── core.py
│   └── py.typed
├── tests/
│   └── test_core.py
└── setup.py

3.2 py.typedファイル

ライブラリが型指定に対応していることを示すため,mylib/ 直下にpy.typedという空ファイルを用意する.特にファイルの中に何か書く必要はない.

3.3 プロジェクトファイルの設定

mypyの設定は,プロジェクトルートに配置されている設定ファイルで実施する.詳細はドキュメントを参照してほしい.Pythonのプロジェクトファイルであるpyproject.tomlsetup.cfgsetup.pyに設定するか,mypy専用の設定ファイルmypy.iniに設定する.ここではこれらの複数のパターンを紹介する.

  • mypy.initによる構成

    以下のようなtoml形式のファイルを準備する.

      # mypy.ini
      [mypy]
      python_version = 3.10
      strict = True
      ignore_missing_imports = True
        
      [mypy-tests.*]
      disallow_untyped_defs = False
    
    • strict = True: より厳密なチェックを有効化
    • ignore_missing_imports = True: 外部ライブラリの型が未定義でも無視する(必要に応じて)
  • setup.cfgによる構成

    setup.cfg内に以下のような追加設定を入れる.ただしもうドキュメンテーションに細かい設定方法が書いてないのと,手元だとstrictのようなオプションは指定しても効いてなさそうなので,素直にpyproject.tomlに移行した方が良いかもしれない.(要検証)

      [options.package_data]
      mlwc = py.typed
        
      [tool.mypy]
      mypy_path = src
    
  • pyproject.tomlによる構成

    pyprojectの場合,セクション名をtool.mypy とする.パッケージごとに追加の細かい設定をする場合はtool.mypy.overrides で追加設定を記述する.

      # mypy global options:
        
      [tool.mypy]
      python_version = "3.10"
      warn_return_any = true
      warn_unused_configs = true
      exclude = [
          '^file1\.py$',  # TOML literal string (single-quotes, no escaping necessary)
          "^file2\\.py$",  # TOML basic string (double-quotes, backslash and other characters need escaping)
      ]
        
      # mypy per-module options:
        
      [[tool.mypy.overrides]]
      module = "mycode.foo.*"
      disallow_untyped_defs = true
        
      [[tool.mypy.overrides]]
      module = "mycode.bar"
      warn_return_any = false
        
      [[tool.mypy.overrides]]
      module = [
          "somelibrary",
          "some_other_library"
      ]
      ignore_missing_imports = true
    
  • setup.pyによる構成

    近年はもう非推奨になっているsetup.pyだが,これでも一応mypyの設定は可能.

      # setup.py
      from setuptools import setup
        
      setup(
         package_data={"package_a": ["py.typed"]},
          ...
          options={
              'mypy': {
                  'python_version': '3.10',
                  'strict': 'True',
                  'ignore_missing_imports': 'True',
              }
           
    

3.4 チェックの実行

チェックは今まで通りmypyコマンドで実行する.

mypy mylib/

まとめ

今回は,mypyの紹介とともに,自身の自作ライブラリにmypyを反映させるために必要だった設定をまとめた.

  • mypy はPythonの型ヒントに基づいて静的型チェックを行うツールである.
  • 自作ライブラリではpy.typedを含めることで型付きライブラリとしてmypyに認識させる必要がある.
  • 自作ライブラリでは,設定ファイルに設定を追加する必要がある.

他のフォーマッタ,リンターと合わせたCI/CD向けの設定等についてはまた追って記事にしようと思う.