Python
python3
mypy
KLabDay 21

mypyやっていったぞ

これはKLab Advent Calendar 2017の21日目の記事です。
昨年のKLab Advent Calendar 2016mypyやっていくぞという記事を書きましたが、やっていったのでその続きを書きます。

typingとmypyの更新

去年の時点ではmypy0.4.6でしたが、12月20日の時点でmypyは0.560, pythonは3.6.4がリリースされています。
やっていったプロジェクトではmypyは更新があるたびにバージョンを上げていました。
更新時に目についた機能を紹介します。

mypyはmypyになった

mypyはmypyというパッケージ名になりました。以前までは名前が衝突していたためmypy-langというパッケージ名でした。

pip install mypy

でインストールできます
http://mypy-lang.blogspot.jp/2017/01/mypy-0470-released.html

PEP-526の変数アノテーション

Python3.6からPEP 526が採用され、変数のアノテーション構文が入りました。mypyもこれに対応しています。

変数アノテーション使ってmypyで引っ掛ける例

from typing import ClassVar

#  モジュール変数
module_variable: int = ""  # intにstr入れようとしてエラー # error: Incompatible types in assignment (expression has type "str", variable has type "int")


class Sample:

    # クラス変数
    class_variable: ClassVar[int]
    # デフォルト値付クラス変数
    class_variable_default: ClassVar[int] = ""  # intにstr入れようとしてエラー # error: Incompatible types in assignment (expression has type "str", variable has type "int")
    # インスタンス変数
    instance_variable: int

    def __init__(self) -> None:
        self.instance_variable = ""  # intにstr入れようとしてエラー # error: Incompatible types in assignment (expression has type "str", variable has type "int")


Sample.class_variable = ""  # intにstr入れようとしてエラー # error: Incompatible types in assignment (expression has type "str", variable has type "int")

typing.NamedTuple

型付き版namedtupleとしてtyping.NamedTupleがありました、これに変数アノテーションを付ける文法がサポートされています。
NamedTuple

3.6未満だと

from typing import NamedTuple

Sample = NamedTuple('Sample', [('var1', int), ('var2', int)])

と書いていましたが、3.6.1以降だと

from typing import NamedTuple


class Sample(NamedTuple):
    """type annotated namedtuple sample"""  # 3.6.1以降だとdocstringsが書ける
    var1: int
    var2: int

    def total(self) -> int:  # 3.6.1以降だとメソッドが書ける
        return self.var1 + self.var2

このように書けます。ちなみに
3.6以降で変数アノテーションが使え、3.6.1以降でメソッドとdocstringsが使えます。

Protocols

Structural subtypingというものがtypingでサポートされます。PEP 544

これは既存の継承を使った方法とは別に、クラスに生えるメソッドのシグネチャを制約するもので、
定義されたProtocolと同じメソッドを持ったクラスを静的解析ツールがProtocolのサブタイプとみなしてチェックできるようになります。
ランタイムに影響を与えず、静的解析で確認できるところが嬉しいですね。mypyではすでにサポートされています。

言葉だけだと説明が難しいのでIterableを実装する例をあげます。

from typing import Iterable, Iterator

class IntIterable:

    def __iter__(self) -> Iterator[int]:
        return iter([1, 2, 3])


def consume_iter(iterable: Iterable) -> None:
    for i in iterable:
        print(i)


consume_iter(IntIterable())

IntIterableIterableを継承していませんが、__iter__を実装しているためIterableを引数に取るconsume_iterに渡すことができます。

また自前でProtocolを定義することもできます。

from typing_extensions import Protocol  # 将来的にtypingからインポートできるようになるはず


class Reader(Protocol):

    def read(self) -> str:
        ...

class TextReader:

    def read(self) -> str:
        return "hoge"


def message_reader(reader: Reader) -> None:
    print(reader.read())


message_reader(TextReader())  # OK 明示的にReaderを継承してないけど`read`メソッドを持ってると通る
message_reader(open("somefile"))  # OK ビルトインなクラスでもOK
message_reader(1) # Argument 1 to "message_reader" has incompatible type "int"; expected "Reader"

今のところPEP 544 のステータスはDraftですがtyping_extentionsをインストールすると使うことができます。
もちろんmypyでもチェックしてくれます。
http://mypy.readthedocs.io/en/latest/class_basics.html#protocols-and-structural-subtyping

ファイルベースコンフィグ

コマンドライン引数だけではなくファイルで設定を指定できるようになりました。
コマンドラインとは違いモジュール単位で設定できるようになっています。
https://mypy.readthedocs.io/en/latest/config_file.html#the-mypy-configuration-file

どうやっていったか

去年は一旦ビジネスロジックの詰まったサブパッケージにmypyを実行し通るようにしました。
CIに組み込む必要があるなと思いつつも、コードはバリバリと更新されていたので導入するタイミングが難しかったです。
そこで、さらに小分けにして取り込んでいくことにしました。
対象のモジュールを羅列したファイルを指定して実行する方法があったのでこれを採用しました。
https://mypy.readthedocs.io/en/latest/command_line.html#reading-a-list-of-files-from-a-file

mypy_check_filesに

package/module.py
package/sub_package1
package/sub_package2/module.py

このように書き

mypy @mypy_check_files

と実行するとmypy_check_filesに記載されたファイル・ディレクトリを走査して実行してくれます。
これをそのままCIの設定に突っ込んで、mypy_check_filesに対応したファイルを順次追記して取り込んでいきました。
対応できたモジュールから順次取り込めるので、便利ですね。無理にこの仕組を使わなくても、シェルスクリプトでやってもいいと思います。

途中修正したものとか

warn-no-return

returnが無いと怒られるパターンが幾つかあったので修正しました。

def hoge() -> Optional[int]: # 行末にreturnがなくてエラー  # error: Missing return statement
    if cond1:
        return 1
    if cond2:
        return 2

return書いたほうが明確でよさそう
--no-warn-no-returnオプション付けると抑制することもできます

error: Return value expected

from typing import Optional


def hoge(s: str) -> Optional[int]:
    if s.startswith("1"):
        return 1
    if s.startswith("2"):
        return 2
    return  # `return expression`と`return`が関数内で混ざってる # error: Return value expected


a = hoge("")

関数中でreturn expressionreturnを混ぜるなということらしいです。
https://github.com/python/mypy/issues/1003
PEP8に書いてあるのですが、初めて知りました。

import but not use

去年に引き続きこの問題は残っていますが、
PEP 526とflake8(PyFlakes)の更新でだいぶ楽になりました。

from typing import Iterator  # OK
from typing import List  # OK
from typing import Dict  # マジックコメントでしか使ってないのでflake8エラー # error: F401 'typing.Dict' imported but unused

i: Iterator = iter([])


def f() -> None:
    x: List
    x = []
    print(x)
    return None


f()

d = {}  # type: Dict

このスクリプトにflake8(3.5.0)を走らせるとDictが引っかかりまが、PEP 526で対応した部分は通ります。

Sqlalchemy

まだ、対応できてません。
なので、Sqlalchemyのテーブル定義やクエリの詰まったパッケージはチェックしてません。
本体のIssueではpluginでなんとかする方向なのですが、pluginの実装は現在Experimentalです。

参考
https://github.com/python/mypy/pull/3517
https://github.com/python/mypy/issues/3538

行長過ぎる問題

コメント形式・PEP 526形式どちらを付けるにしても、一行あたりの文字数が増えてしまいflake8に怒られるパターンが多々ありました。
今回は気合で改行して乗り切りましたが、もうすこし条件を緩くしても良かったかもしれません。

まとめ

去年に引き続きmypyをやってきましたがtypeshedの不整合は少なくなり、
mypy自体の細かいバグも目に見えるところでは少なくなっていて、日常で使う分には問題なくやっていけると思います。
3.7向けのtyping関連のPEPも多くあり、これからも新しい仕組みが入ってくるでしょう。
新しくプロジェクトを立ち上げる際には導入を検討していいとおもいます。

参考 3.7向けtyping関連PEP

https://www.python.org/dev/peps/pep-0560/
https://www.python.org/dev/peps/pep-0561/
https://www.python.org/dev/peps/pep-0562/
https://www.python.org/dev/peps/pep-0563/
https://www.python.org/dev/peps/pep-0544/

おまけ

dropboxがpyannotateというツールを公開しました
https://github.com/dropbox/pyannotate
http://mypy-lang.blogspot.jp/2017/11/dropbox-releases-pyannotate-auto.html

これは勝手にアノテーション付けてくれる君で、アノテーションついてないコードベースへ導入する際に実行するとコメントベースのアノテーションを付けてくれます。
テストコードなどを使い実際に動かしてランタイムで型を判定する仕組みらしいです。
手元で試したところ、既にアノテーションついている箇所にも追加されてしまいました。
既存のコードベースへタイプアノテーションを導入する際は試してみてもいいと思います。

おまけのおまけ

去年書き忘れてたので言いたかった
typeshedに投げてたPRマージされたよ!!
https://github.com/python/typeshed/pulls?q=is%3Apr+is%3Aclosed+author%3Ak-saka