BLOG
プログラミング

【Python #1】はじめてのmypy

こんにちは! 仙台オフィスで主にTypeScriptを書いているChloéです。

普段の開発では静的型付き言語を使うことが多いのですが、今回はPythonにおける型ヒントと、それに基づいて型チェックを行うツールのmypyについて記事を書いてみることにしました。

記事のターゲットとしては、主に「Jupyter Notebook等で生成AI等を使うためにPythonを書くのには慣れているけど、Pythonで(Web)アプリケーションを作った経験があまり無い」という方を想定しています。

目次

  1. 環境
  2. Pythonに型チェックを導入するモチベーション
  3. 型ヒントの基本
    1. 変数の型ヒント
    2. 関数の型ヒント
    3. Union型
  4. mypyの導入
    1. mypyのインストール
    2. mypyによる型チェック
  5. 実践的な型チェック
    1. 意図しない型変換
    2. Null安全
  6. まとめ

1. 環境

  • Python 3.12.1
  • mypy 1.10.0
    • 本記事では特にカスタマイズはせず、デフォルトの設定で使用します。

2. Pythonに型チェックを導入するモチベーション

私たちの会社では、生成AIや機械学習関連の業務がそこそこ多いため、Pythonエンジニアの割合が高いです。そのため、Webバックエンドアプリケーションは主にPythonで実装されています。Pythonは動的型付き言語であるため、ある程度の規模以上のアプリケーションを実装する際、型の情報がコードに明示されていないとメンテナンス性に問題が生じる可能性があります。

このような問題を解決するために、現代のPythonには 型ヒント(Type Hints) という機能が存在します。型ヒントを利用することで、以下の利点が得られます。

  • ドキュメンテーション
    • 型ヒントによって変数や関数にどのような値が入るかが可視化されるため、人間がコードを読んだ時に理解の助けになります。
  • 自動補完
    • Pythonに対応しているテキストエディタやIDEについて、自動補完をサポートします。Pythonは動的型付き言語なので、ある変数についてどのような操作や演算ができるか解析しにくいです。型ヒントを入れることにより、変数に対する操作や演算が明確になり、自動補完がより正確になります。
  • 型チェック
    • 型をチェックできる静的解析ツールにより、プログラミングのミスに早期に気付くことができます。

型ヒント自体はPythonの機能ですが、Pythonのランタイムは型ヒントに対して特に何もしないことに注意してください。(型ヒントが書かれているコードを実行した時、型ヒントが無い場合と全く同じ動作をします) 自動補完や型チェックについては、型ヒントを理解する外部ツールによって実現されています。

この記事では型チェックにフォーカスし、mypyという静的解析ツールを使って実際にPythonコードの型チェックを行う方法を説明します。

3. 型ヒントの基本

本章では、Pythonのコードに型ヒントを付ける方法を扱います。

3.1 変数の型ヒント

  • 変数に型ヒントを付ける際は、 変数名: 型名 = 値 と書きます。
# int型の値
int_value: int = 123

# str型の値
str_value: str = "Biz Freak"

# bool型の値
bool_value: bool = True
  • listtupleといったコレクション型の型ヒントは以下の要領で書きます。
# str型の要素を持つlist
list_value: list[str] = ["abc", "def", "ghi"]

# 1番目の要素がstr型, 2番目の要素がint型のtuple
tuple_value: tuple[str, int] = ("abc", 1)

# Keyがstr型、Valueがint型のdict
dict_value: dict[str, int] = { "abc": 1, "def": 2 }

3.2 関数の型ヒント

関数に型ヒントを付ける際は、 def 関数名(引数名: 型名 ...) -> 戻り値の型名 と書きます。

# str型の引数1つをとってstr型の返り値を返す関数
def do_freak(something: str) -> str:
  return f"{something} Freak"

3.3 Union型

複数の型を受け入れる変数を定義したいことがしばしばあります。そのような時には、Union型を使います。Union型は「複数の型のうちいずれかの型」を表現するものです。

int_or_str_value: int | str = 1 # intとstrのUnion型。 int型の値を代入
int_or_str_value = "foo" # str型の値の代入が許される

4. mypyの導入

本章では、静的解析ツールであるmypyの導入と、簡単な型チェックの方法を扱います。

4.1 mypyのインストール

mypyは外部ツールなので、 pip などのパッケージマネージャからインストールするのが簡単です。

pip install mypy

4.2 mypyによる型チェック

mypy コマンドの後に型チェックしたいスクリプトファイルのパスを渡すと、そのファイルの型チェックを行えます。

mypy my-script.py

ここで、標準出力に Success: no issues found in 1 source file と出力されると、型チェック成功です。

また、ソースコードが置かれているディレクトリを渡すと、ディレクトリの中のスクリプトファイルを再帰的に型チェックできます。

mypy ./src/

4.3 テキストエディタ・IDEとの統合

この手のツール全般に言えることですが、テキストエディタやIDEと統合することで快適な開発環境を構築することができます。ここでは例として、VSCodeとの統合方法を紹介します。

  • VSCodeの拡張機能のマーケットプレースから”Mypy Type Checker”をインストールします。
  • 拡張機能の設定から”Mypy-type-checker: Show Notifications”を”onError”に設定します。

  • 型エラーのある箇所に、自動的に以下のような表示が出ます。

    このエディタは正確にはVSCodeではなく、VSCodeをフォークして開発されたCursorです:-)

5. 実践的な型チェック

この章では、実際のコードでありそうな型エラーとその解決方法について扱います。

5.1 意図しない型変換

5.1.1 型エラーの出るコード例

以下は、価格と税率から税込価格を計算しようとしているコードです。

# 税込価格を計算する
def calcurate_total(price: int, tax_rate_percentage: int) -> int:
	tax_rate = tax_rate_percentage / 100
	return price * (1 + tax_rate)

if __name__ == "__main__":
    print(calcurate_total(900, 10))

このコードをmypyで型チェックすると、以下のようなエラーが出ます。

error: Incompatible return value type (got "float", expected "int")  [return-value]

5.1.2 原因

原因は price * (1 + tax_rate)の部分です。 int 型の値 price と、 float 型の値 tax_rate について計算を行っていますが、このとき price の型が int から float に自動的に変換されます。

  • floatintよりも表現できる範囲が広いため、これらを混ぜて演算するとintfloatに変換された上で演算されます。このような挙動を型昇格といいます。

5.1.3 修正案

修正の方針ですが、税込価格が整数で欲しい場合は以下のような修正が考えられるでしょう。

# 税込価格を計算する
def calcurate_total(price: int, tax_rate_percentage: int) -> int:
	tax_rate = tax_rate_percentage / 100
	return math.floor(price * (1 + tax_rate)) # 小数点以下を切り捨て

この場合、math.floor()float型を引数に取ってint型を返す関数なので、calcurate_total関数の戻り値はint型になります。

割りとありがちなケアレスミスですが、型ヒントを書いておけばこのようなミスに早く気付くことができます。

5.2 Null安全

5.2.1 型エラーの出るコード例

以下は、辞書からキー”id”に対応する値を取得しようとしているコードです。また、関数内で何やらデバッグログを出そうとしています。

import logging
logging.basicConfig(level=logging.DEBUG, format="%(message)s")

def get_id_value(input: dict[str, str]) -> str:
    id_value = input.get("id")
    logging.debug("id_value: " + id_value)
    return id_value

if __name__ == "__main__":
    print(get_id_value({"id": "bar"}))

このコードをmypyで型チェックすると、以下のようなエラーが出ます。

Unsupported operand types for + ("str" and "None")  [operator]
Incompatible return value type (got "str | None", expected "str")  [return-value]

5.2.2 原因

エラーの内容を大雑把にいうと

  • 1行目: str型と None型について”+”演算子を適用できない (つまり、足し算ができない)
  • 2行目: 関数の戻り値の型ヒントがstrだが、実際戻り値になりうる型は str | None である

という内容です。

input.get("id") について、inputに “id”という名前のキーが含まれていない場合、対応する値の代わりにNoneが返されるのですが、このコードはそれの考慮を忘れてしまっています。

また、このコードですが、get_id_value関数にうっかり”id”というキーを含まない辞書を渡して実行すると以下のような例外がraiseされてしまいます。

Traceback (most recent call last):
  File "/path-to-script.py", line 20, in <module>
    print(get_id({"a": "foo", "d": "bar"}))
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/path-to-script.py", line 15, in get_id
    logging.debug("id_value: " + id_value)
                  ~~~~~~~~~~~~~^~~~~~~~~~
TypeError: can only concatenate str (not "NoneType") to str

このような実行時エラーが起こりうる実装を、mypyを使うことによって実行前にチェックできます。

5.2.3 修正案

まず、logging.debug()の部分で例外が出ないように修正します。f-stringsを使い、変数を文字列にそのまま埋め込むのが手っ取り早いでしょう。

logging.debug(f"id_value: {id_value}")

次に、戻り値の型を修正します。今回は ”id” がない場合はとりあえずNoneを返すようにします。この時、適切な型はstrNoneUnion型です。

def get_id_value(input: dict[str, str]) -> str | None:
  ...

合わせると、以下のようになります。

import logging
logging.basicConfig(level=logging.DEBUG, format="%(message)s")

def get_id_value(input: dict[str, str]) -> str | None:
    id_value = input.get("id")
    logging.debug(f"id_value: {id_value}")
    return id_value

if __name__ == "__main__":
    print(get_id_value({"id": "bar"}))

これは極端な例でしたが、値がNull(PythonではNoneに相当する)である場合のハンドリングを忘れてしまうことはしばしばあります。ある変数がNullではない事を言語レベルで表明できる機能のことを「Null Safety」と呼んだりしますが、mypyによってそれを部分的に実現できます。

6. まとめ

この記事では、Pythonの型ヒントとmypyを用いた型チェック方法を説明しました。型ヒントを使用すると、コードのドキュメンテーション、自動補完、型チェックの利点が得られ、開発効率が向上します。また、mypyを使用することにより、型エラーを早期に発見し修正することが可能になります。

また、一般的にコードを書く時間よりも読む時間の方が長いといわれています。型ヒントを重要な場所(特に、モジュール外から参照されうる関数や変数)に書くことにより、 他の開発者がコードを読む際に理解が容易になります。それにより、特に多人数で開発する際の開発者間の齟齬が小さくなり、結果的に開発の高速化に寄与できるのではないでしょうか。

どのようなツールを使用するかはプロジェクトやチームの状況によりますが、mypyや型ヒントなどのツールは、Python開発をさらに効率的で信頼性の高いものにするための一つの選択肢となるでしょう。

株式会社Biz Freakは、ロバストなソフトウェアの設計・実装に興味のあるPythonエンジニアを募集しています。

BACK

RECRUIT

世の中に「技術」で
価値を生み出す

JOIN OUR TEAM