Python開発: mypyによる型チェックでコードをもっと堅牢に

# はじめに

Pythonはそのシンプルさと柔軟性から、多くの場面で利用される人気の高いプログラミング言語です。
しかし、**動的型付けであるため、型に関連するバグが実行時まで発見されないというリスク**があります。
これが静的型付け言語を好む開発者にとって、Pythonを敬遠する理由のひとつであったりします。

ただし、Python 3.5以降に導入された**型ヒント**と、それを活用する型チェックツール**mypy**を組み合わせることで、型安全性を大きく向上させることができます。
本記事では、静的型チェックの利便性と強力さを提供するmypyについて詳しく解説します。

## mypyとは？

**mypy**はPythonコードの静的型チェックを行うツールです。
Python 3.5以降で導入された**型ヒント**を活用して、コード内の型の整合性をチェックします。
これにより、潜在的なバグを事前に発見しやすくなり、コードの安全性と保守性を向上させることができます。

## mypyを導入するメリット

1. **バグの早期発見**
型チェックを行うことで、コード中の潜在的なバグを実行前に防ぐことができます。
特に大規模プロジェクトや複数人での開発では、異なる部分のコードが予期しない型で相互作用する可能性が高まります。
mypyを使えば、これらの問題を早期に検出できるため、修正コストを抑えることが可能です。
2. **コードの可読性向上**
mypyのオプション(例えば`--strict`)を使うと**型定義を強制**できるため、すべての関数やメソッドに型ヒントを記述する文化が定着します。
型ヒントは他の開発者がコードを理解する助けになるため、チームでの開発効率が向上します。
3. **型チェック処理を省く**
型チェックをコード内で明示的に行う処理を省くことができます。
従来、関数内部でisinstanceやカスタム例外処理を使って型を検証することが多く見られましたが、mypyを導入すればこれらのチェックは不要になり、コードの記述量が減少し可読性も向上します。
例えば以下のコードが不要になります：

### 従来の型チェックコード
```python
def add_numbers(a, b):
    if not isinstance(a, int) or not isinstance(b, int):
        raise ValueError("エラーコメント")
    return a + b
```

### mypy導入後のコード
mypyで引数がint型であることを保証しているため、型チェックを省くことができる
```python
def add_numbers(a: int, b: int) -> int:
    return a + b
```

## mypyの使用方法
mypyのインストールと基本的な使い方を以下に示します。

### 1. インストール
まず、mypyをインストールします。
```bash
pip install mypy
```

### 2. 型ヒントを追加する
コードに型ヒントを記述します。
```python:sample.py
def greet(name: str) -> str:
    return f"Hello, {name}!"
```

- `name: str` は引数`name`が文字列であることを示します。
- `-> str` は戻り値が文字列であることを示します。

### 3. mypyで型チェックを実行する
以下のコマンドで静的型チェックを実行します：

```bash
mypy sample.py
```

型の不一致がある場合、エラーが表示されます。
例えば、以下のコードをチェックした場合

```python
def greet(name: str) -> str:
    return 123  # 戻り値がint型
```

実行結果：

```bash
sample.py:2: error: Incompatible return value type (got "int", expected "str")
Found 1 error in 1 file (checked 1 source file)
```

エラーメッセージは「戻り値の型が期待していたstrではなく、intである」と指摘しています。このように、mypyはコードの問題箇所を的確に教えてくれます。

## mypyの便利なオプション
**mypy**には便利なオプションが豊富に用意されています。
今回はその中でも便利そうなオプションをピックアップして紹介します。

### `--strict`
推奨されるすべての型チェックオプションを有効にします。
オプションを渡していないデフォルトのmypyでは、**型を明示的に設定していない関数やメソッドに対する警告**や**曖昧な型に対する警告**が表示されませんが、`--strict`を使用することで、型注釈の不足や曖昧な型に関する警告が有効になります。

これにより、厳格な型チェックが適用され、コードの型安全性が向上します。
一方で、既存のコードベースに適用すると警告が増えるため、段階的な導入がおすすめです。

#### オプションを渡していないデフォルトのmypyの場合
以下のコードは、型を明示していない関数や、曖昧な型(listなど)が含まれています。
```python:sample.py
def add_numbers(a, b):  # 型を明示していない
    return a + b

def print_list(data: list) -> None:  # 型を明示していない
    print(data)
```
このコードに対して`mypy sample.py`を実行しても、特に警告やエラーは表示されません。
```bash
% mypy sample.py
Success: no issues found in 1 source file
```

#### `--strict`オプションを渡した場合
一方、`mypy --strict sample.py`を実行すると、以下のようにエラーが表示されます。
```bash
% mypy --strict sample.py
sample.py:1: error: Function is missing a type annotation  [no-untyped-def]
sample.py:4: error: Missing type parameters for generic type "list"  [type-arg]
Found 2 errors in 1 file (checked 1 source file)
```
このように、`--strict`を使用することで、**型注釈が不足している関数**や、**曖昧な型**について警告が表示されるようになります。

##### エラー解決後のコード
以下のように型を明示すると、`--strict`を有効にしても警告がなくなります。
```python
def add_numbers(a: int, b: int) -> int:  # 引数と返り値に型を明示
    return a + b

def print_list(data: list[str]) -> None:  # listの中身の型を明示
    print(data)
```

### `--disallow-any-expr`

**明示的に指定されたAny型**の利用を禁止します。

:::note warn
このオプションは2024/12/15現在の最新バージョン(mypyバージョン1.13.0)では`--strict`モードには含まれていないため、利用時には別途オプションを渡す必要があります。
:::

以下はサンプルコードと実行した例です。
```python:sample.py
from typing import Any

def greet(name: Any):
    print(f"Hello, {name}")
```

このコードを`mypy --disallow-any-expr sample.py`で実行すると、次のような警告が表示されます

```bash
% mypy --disallow-any-expr sample.py 
sample.py:9: error: Expression has type "Any"  [misc]
Found 1 error in 1 file (checked 1 source file)
```


### `--warn-unreachable`
**到達不能コード**を警告するオプションです。

:::note warn
このオプションは2024/12/15現在の最新バージョン(mypyバージョン1.13.0)では`--strict`モードには含まれていないため、利用時には別途オプションを渡す必要があります。
mypyのissueに[Enable --warn-unreachable in --strict in Mypy 2.0](https://github.com/python/mypy/issues/18078)が起票されているため、今後`--strict`に含まれてくる可能性があります。
:::

以下はサンプルコードと実行した例です。
```python:sample.py
def check_number(x: int) -> str:
    if isinstance(x, str):  # 到達不能: 型ヒントで x は int 型と指定
        return "String"
    return "Integer"
```
このコードを`mypy --warn-unreachable sample.py`で実行すると、次のような警告が表示されます。
```bash
% mypy --warn-unreachable sample.py 
sample.py:2: error: Subclass of "int" and "str" cannot exist: would have incompatible method signatures  [unreachable]
sample.py:3: error: Statement is unreachable  [unreachable]
Found 2 errors in 1 file (checked 1 source file)
```


## おまけ: mypy.iniでオプションを記載する
mypyはコマンドラインで指定するオプションを設定ファイルで記載することができます。
この設定ファイルはプロジェクトのルートディレクトリに配置することで、毎回コマンドラインでオプションを指定する手間を省くことができます。
以下はサンプルです。
```ini:mypy.ini
[mypy]
# すべての型を厳密にチェック
strict = True

# 到達不能コードを警告
warn_unreachable = True

# 明示的に指定されたAny型の利用を禁止
disallow_any_expr = True
```

## まとめ
今回の記事ではコマンドを手動で実行して型チェックを行いましたが、pre-commitフックなどと組み合わせることで自動的に型チェックを行うことも可能です。
mypyの型チェックを活用して、Pythonコードをより堅牢で保守しやすいものにしましょう！
ご拝読いただきありがとうございました。