mypyやっていくぞ

これは[KLab Advent Calendar 2016](http://qiita.com/advent-calendar/2016/klab) 21日目の記事です。

mypyやっていく気持ちになったので、社内のコードで実践してみた話を書きます。

### 続編
[mypyやっていったぞ](https://qiita.com/k-saka/items/9d7111e82cd06e5419db)

## mypyとは
python3.5から型アノテーションを付ける構文([PEP 484](https://www.python.org/dev/peps/pep-0484/))が追加されました。このアノテーションをもとにコード上の型を静的解析してくれるのが[mypy](http://mypy-lang.org/)です。

### 例
```python
def func() -> int:
    return "a"
```
このようなコードにmypyを走らせてみると

```
$ mypy test.py
test.py: note: In function "func":
test.py:2: error: Incompatible return value type (got "str", expected "int")
```
アノテーションを元に型をチェックして、戻り値の型がintなのにstrを返しているのでエラーになります。


## やっていく気持ち
"mypyやっていく"というのは、PEP 484に対応した型アノテーションを付け、mypyを実行し静的チェックをパスすることを指します。なぜやっていく気持ちになったかというと、[Static types in Python, oh my(py)!](http://blog.zulip.org/2016/10/13/static-types-in-python-oh-mypy/) この記事を読んだことが大きいです。やっていけそうな気がしました。担当している案件が規模が大きく既存機能の変更頻度も高いものだったので、信頼できる型アノテーションが有ると、修正する際の助けになりそうだなと思いました。
その他にはPyCharmがPEP 484に対応し警告を出してくれるようになったことで間違ったType Hintingが付けられてる箇所がエラーになり、目につくようになったからというのもあります。

## スタートライン
コードベースはpython3.5で書かれており、"アノテーションは付けたい人が付ける"ぐらいの緩いルールで開発されていました。アノテーションはバッチやらテストやらを除くと7割ぐらいのメソッドには付いているかなといったところです。とりあえず全体に
`mypy -s --fast-parser --strict-optional --disallow-untyped-defs --disallow-untyped-calls <dir>`
を実行してみると3000件近いエラーが出ました。このまま上から順番に潰していく作業は心が折れる可能性があるので、一旦ビジネスロジックの詰まった部分だけに限定し、最小限のオプションのみで実行し修正することにしました。

## やっていく
mypyのversionは0.4.6, pythonは3.5.2です。
`mypy -s --fast-parser <dir>`
を実行しつつ修正しました。
`-s`オプションはimportしたモジュールのチェックを行わなくするオプションで、これがないとimportしたサードパーティー製のライブラリまでチェックしに行ってしまいます。`<dir>`で指定したディレクトリ中のモジュールimportはよしなにしてくれます。外部モジュールの関数を呼び出した場合の引数,戻り値の型は`Any`になります。
`--fast-parser`オプションはそのうちデフォルトになるオプションで、現行のデフォルトパーサーだと引数のアンパック周りなどでパースエラーになるパターンがあります。python3.6のシンタックスに対応しているのも`--fast-parser`のみです。

## やってみて躓いたところなど

### `__init__`にアノテーション付け忘れてる
クラス中の`__init__`メソッドの型アノテーションを付けていない箇所がとても多くありました。そもそも`__init__`にアノテーション付ける意識がなかったです。
> Note that the return type of __init__ ought to be annotated with -> None　.

[PEP484#the-meaning-of-annotations](https://www.python.org/dev/peps/pep-0484/#the-meaning-of-annotations)
とのことなので戻り値の型は`None`にしましょう。幸いこのパターンは機械的に対応可能なので、適当なスクリプトを書いて対応しました。

### imported but unused

pythonの静的解析ツールに[flake8](http://flake8.pycqa.org/en/latest/)というものがあり、これを使いCIサーバーでチェックをかけています。flake8のテストケースには未使用のimport文を確認するものがあり、インポートしたものの一度も使っていない場合エラーになります。ここで問題になるのが、関数ではなく変数にアノテーションを付ける場合です。python3.5では変数にアノテーション付ける場合コメントで対応することになっています。(python3.6では新たに変数アノテーションを付ける構文が追加されます。[PEP526](https://www.python.org/dev/peps/pep-0526/))

```python
from typing import Optional
a = None  # type: Optional[int]
```
このように変数アノテーションを書くのですが、コメントになっているので`flake8`を実行すると

```
$ flake8 ./test.py
./test.py:1:1: F401 'Optional' imported but unused
```
エラーになってしまいます。なので、このエラーがでたモジュールのimport部分には、`# noqa`とコメントを追加してエラー抑制することで対応しました。

```python
from typing import Optional  # noqa
a = None  # type: Optional[int]
```
python3.6からは

```python
a: Optional[int] = None
```
と書けるので、変数アノテーションによる未使用インポートの問題は出にくくなるかもしれません。


### 循環import
モジュール変数やメソッドにアノテーションを付けているとモジュール間で相互importしてしまうパターンがあります。こうなると実行時にループしてエラーが起きてしまうので対策が必要です。python3.5.2から[`typing.TYPE_CHECKING`](https://docs.python.org/3.5/library/typing.html#typing.TYPE_CHECKING)という変数が使えるようになっており、これはサードパティー製のツールが読むときのみ`True`にする変数で、これを使って回避することができます。この変数を使いimport側で対象のモジュールを解析時のみ読み込むようにして回避します。
mypy公式のドキュメントにも解決策として載っています。
http://mypy.readthedocs.io/en/latest/common_issues.html#import-cycles

```python
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from module import MyClass  # noqa


def func() -> "MyClass":
    return MyClass()
```

### `# type: ignore`の位置
`# type: ignore`というコメントを付けるとmypyのテストを抑制することができるのですが、この位置が曲者でした。複数行にまたがる式の中にエラーが有った場合これを抑制するには式の最初の行にコメントを付ける必要があります。また、ここで付けたコメントはその後の行にも影響が出てしまいます。

```python
class MyClass:
    def __init__(self, a: int, b: str) -> None:
        self.a = a
        self.b = b

MyClass(1,  # type: ignore
        1)  # この行のエラーを無視したい場合、↑にコメントを付ける必要がある
```
https://github.com/python/mypy/issues/1032
Issueには載っていて、いつか行ごとに`ignore`できるようになるかもしれません。

### Typedshed
mypyはソース中のアノテーションだけでなく型を定義したスタブファイルを読み込むこともでき、標準のライブラリなどはそちらの方法が使われています。それれらのスタブは[typeshed](https://github.com/python/typeshed)で管理されていて、mypyに含まれる形でインストールされます。なので、mypyテスト時はビルトイン関数や標準ライブラリの不正な呼び出しを検知してくれるのですが、スタブ自体はpythonのコードから生成されていて細かい部分で食い違っている場合がまだあります。特定バージョンのソースから生成してしていて、アップデートに追従できてないパターンもあるので見つけ次第PRをぶん投げていくのがいいと思います。

## 解決できていない課題
### SqlAlchemy
動的に属性付けていたりバリバリメタクラスを使っているものだったりすると、アノテーションを付けるのが難しいです。SqlAlchemyの場合テーブル定義するベースクラス中の`__init__`で keyword argumentを受け取って`setattr`しているのでソースに直接アノテーションを付けるのが難しいです。派生クラスで`__init__`定義してやればいいのですが、型アノテーション付けるためにわざわざそれぞれのテーブルで`__init__`を書くかというと躊躇してしまいます。使っている部分はコード上でサブパッケージにまとまっていたので、まるっと対象から外してしまいました。


## やってみて
一通り対応して`mypy -s --fast-parser <dir>`を実行してもエラーが出なくなるところまでたどり着きました。どうしようもないパターンというのはほとんどなくて、大体は素直に直していくことができました。`Typeshed`のバグなどもありましたが`ignore`付けておけばいいのでエラーが消えなくて進めなくなることはありませんでした。
また、幾つかのバグを発見することもできました。テストは書いていたのですが、異状系などテストでカバーできていない場所で見つかりました。修正作業はコードをちゃんと読まないと返している型が判断できないのでそこそこ大変で、毎日少しづつ対応していきました。地道にやっていくしかないでしょう。mypy自体もまだまだ開発中ですが現状でもやってやれないことはないと思います。
コードを読む上で役立っていることは明らかで特に`a = []`といった何が入るかわからない変数にアノテーションがついているのは助けになっています。また、明らかに不正な呼び出しをしている箇所をテストする前に発見できるのは嬉しいことです。リファクタに役立つかはまだわかりませんが、複雑すぎる型を返す関数などが分かりやすくなるので書く際に気をつけるようになりそうです。

## 今後やること
- CIに突っ込む  
  まだCIサーバーでチェックしているわけでないので、メンバーに周知しつつ入れる
- `--strict-optional`をいれる  
  Optionalな型のチェックを厳密にする。[experimental-strict-optional-type-and-none-checking](http://mypy.readthedocs.io/en/latest/kinds_of_types.html?highlight=strict#experimental-strict-optional-type-and-none-checking)
- `--disallow-untyped-defs`をいれる  
   タイプアノテーションのついていない関数をエラーにする
- `--disallow-untyped-calls`をいれる  
  タイプアノテーションのついていない関数呼び出しをエラーにする

という感じで、CIに突っ込みつつ徐々に厳しいオプションにしていけたらいいなあと思います。

## やっていく際に読むといいもの
- [Mypy syntax cheat sheet (Python 3)](http://mypy.readthedocs.io/en/latest/cheat_sheet_py3.html)  
  チートシート。型アノテーションの書き方が分かる
- [Static types in Python, oh my(py)!](http://blog.zulip.org/2016/10/13/static-types-in-python-oh-mypy/)  
  zulipで対応した際のナレッジがつまってる。この記事なんかより読んだほうがいい。  
  Qiitaに翻訳版があります。[[翻訳] Python の静的型、すごい mypy!](http://qiita.com/t2y/items/2a1310608da7b5c4860b)
- [PEP484](https://www.python.org/dev/peps/pep-0484/)  
  チートシートみても分からなくなったら読むといいかも


# **mypyやっていくぞ!!**