多くのPythonコードに型アノテーションしてみたので色々所感を書いてみる

「JavaScriptをTypeScriptへの移行してみた」的な記事を結構見かける一方で、Pythonの型アノテーション関係で近い移行記事などはあまり見ないので「Pythonで型アノテーションってどうなの？」という点などを雑多に記事にしてみました。

# 今まで型アノテーションを使ってきた状況と規模感・前提など

- お仕事では50万行程度の残存するPythonコードで8割強くらいのカバレッジです。ファーストコミットから7年目くらいなので、Python2系から3系へ移行 → 型アノテーションの利用を本格的に開始…としてきたので途中からの型アノテーションの導入です。
- プライベートでは「[apysc](https://github.com/simon-ritchie/apysc)」と「[numdoclint](https://github.com/simon-ritchie/numdoclint)」というPyPI（pip）登録してある2つのライブラリで型アノテーションしてあります。numdoclintは途中からの型アノテーションの利用の開始、apyscは最初から型アノテーションを使う形で進めています。apysc側では約5万行弱くらい（ドキュメントのコードブロックなどを除く）くらいのPythonコードがあるようです。
- 結構古めのPythonをサポートしていたりする都合、Python3.9や3.10などでの新しい書き方などは使っていない所もぼちぼちあります。例えばapyscでは3.6以降を現在サポートとしているため、バックポートが効かない型アノテーションの機能は使っていません。typing_extensionsなどでバックポートが効く機能は利用しています。3.6がEoLになってしばらくしたら最低サポートバージョン上げようかーくらいに緩く考えています。
- 型チェックはmypyとPylance（Pyright）を使っています。他の型チェックのライブラリなどは詳しくありません。
- mypyもPylanceも完全なstrictモードでは対応していません。mypy側はいくつかのstrictの設定を有効化しているといった程度です。

# mypyとPylanceの比較の所感と使い分け

お仕事とプライベート両方ともmypyとPylanceを使っています。ただし以下のように使い分けをしています。

- CIなどの面ではmypy
- VS Code上での型チェックではPylance

このようにしている理由なのですが、まずmypyに関してはpipでさくっとインストールができます。ライブラリ設定なども特に気にせずに、プロジェクト環境のパッケージにmypyをインストールすればさくっと使えるようになります。

一方でPylanceの型チェックをコマンド上で使おうとすると内部のPyright関係のインストールを進める必要があり、Pyrightのインストールがnpmなどが絡んできてきたりと少し面倒に感じる面があります。

逆にVS Code上の拡張機能としてのインストールではPylanceは非常にスムーズにインストールができているものの、mypy側はうまくインストールが進まない・・・という経験をしています（以前の話なので今は改善しているかもしれません。そのうち再度試してみようと思います）。

結果的に楽をしたいと感じてCIなどではmypy、VS Code上での利用に関してはPylance・・・みたいに別のものを利用しています。

ただし両方使ってみた感じ、mypyとPylance間で同じ箇所が引っかかるケースも多いものの別々の箇所で引っかかるケースもぼちぼち経験しています。そのため本当はPyrightなどもCIに組み込んで・・・とやるのがより良いのだろうなとは感じています。こちらも暇になってきたらそのうち仕事などでやってみる・・・かもしれません。

※補足 : この記事を書きつつ一応調べたところ、マイクロソフト公式ではないようですがPyrightのpipでインストールできるパッケージも公開されているようです（未検証）。案外この辺を使うとpipだけを使っているプロジェクトでもさくっと使えるようになるかもしれません。

> Pyright is written in TypeScript and requires node to be installed and is normally installed with npm, this could be a barrier for entry for some python developers as they may not have node or npm, installed on their machine, I wanted to make pyright as easy to install as any normal python package.
> [pyright - PyPI](https://pypi.org/project/pyright/)

# プロジェクトで型アノテーションの利用をスタートする時に反対などは無かったか？

これに関してはチームメンバーの方や自身も含めてPythonを書く前は静的型付け言語を使っていたので型の有用性は説明が不要だったためスムーズに導入となったように感じます。
Pythonのバージョンアップなどで型アノテーションが本格的に使える形になってきたタイミング辺りで自然と利用がスタートしています。

# 安全面が上がるかどうか

これに関しては間違いなく上がったなと思います。というのも、型アノテーションが途中から入ったことによって今まで見落としていたミスなどが結構出てきています。例えば型の不一致やnull安全的なところ（PythonだとNone安全と表記すべきでしょうか・・・？）が担保されていなかったり・・・といったところがちらほらと昔のコードなどで見つかっています。

レビューやテストで見つけられたケースもありますが、人の目だとやはり漏れをゼロにはできないため、やはりデプロイ前にこの辺のチェックがされると安心感があります。

インターフェイスを変えた時なども問題点の把握などがやりやすいですし、自動テストと型チェックの両方が走るというのはテスト単体などよりも一層安心できて良いです。思い切った更新などもかけやすくなった気がします。

少なくとも整備がされた現在、「型アノテーション辛いからやっぱり切り落とそう…」と感じたことは今のところ一度もありません。TypeScriptなども移行後にJavaScriptに戻そう…となるケースは稀だと思いますが、近い感覚かもしれません。

長期プロジェクトでコード量がそれなりの規模になってくる場合にはやはり利用されている方がいいかなという所感です。この辺はDropboxの記事にも書かれているのでそちらもご確認ください。

> 動的に型付けされた Python しか使用したことがなければ、静的型付けと mypy がなぜ話題になるのか不思議に思うでしょう。Python は、動的型付けを備えているから楽しいという部分もあるかもしれませんが、全体を見通しにくくなる場合があります。静的型チェックの鍵は、規模です。プロジェクトが大きくなればなるほど、静的型チェックの必要性を感じるようになります（最終的には必須になります）。
> [Python の型チェックが 400 万行に到達するまで](https://navi.dropbox.jp/our-journey-to-type-checking-4-million-lines-of-python)

# 部分的な移行がやりやすいのは非常に楽で良い

「このモジュールはmypyのチェック対象とする」的な指定をして、そちらに応じてmypyの制御を調整するスクリプトを少し書いたり・・・といった作業は必要になりましたが、しかし言語が変わったりするわけではないので部分的に型アノテーションを利用していく形で進められたのは楽で良いなと思いました。

プロジェクトの全コードを一気に型アノテーションしないといけない・・・となると結構大変ですし他のタスクも長期間止まってしまいます。他のタスクが止まってしまうと型アノテーション対応を行うための工数的な許可をいただいたり着手のハードルが高くなってしまいます。

そういった意味では部分的に移行ができる形というのは毎日数モジュールずつ対応・・・みたいなことができて気軽に着手できますし、他のタスクの作業が長期間止まったりもしません。ずっとその作業に縛られたりもしないため飽きにくいというメリットもあります。

日々型アノテーションのカバレッジも確認できるようにもしているのですが、段々とカバレッジが増えて安全性が今までよりも増してじわりじわりと改善していっている・・・というのは気分が良いものです。

# 型アノテーションを途中から追加していくのは大変では無かったか？

コード量は多いと言えばそれなりには多いので時間はかかります。

ただし前述の通り少しずつ着手していけましたし、前段階として単体テストはほぼほぼの行カバレッジは確保できていた点、numdoclintという自前のLintでdocstringで型情報などは書くのを必須としていたので他人のコード含め型アノテーションを追加していくのは特に大変ではなく事故も無く・・・といった感じでした（とにかく手をたくさん動かす必要はありましたが・・・）。

numdoclint参考記事:

https://qiita.com/simonritchie/items/84c4d4b2528309c30359

一方で、前節で触れたように明らかに昔のコードがミスっている・・・という点が型アノテーションを手動でやっていると見つかったりもします。このケースでは少し厄介で、テストや場合によっては参照箇所なども含め修正が必要になったりもしてきます。こればかりはしょうがないですし、ある種問題が顕在化していないバグを事前に潰せた・・・と思って前向きに時間を割いて対応が必要になるケースもぼちぼちありました（この辺は少し大変かなと思います）。

# 日々型アノテーションをしていると、知らなかった仕様が地味に出てくる

大分ネット上の記事も充実してきた感じもありますし有難いことにまとめ記事なども他の方が投稿していらっしゃるので以前と比べるとつらみが減った感じはありますが、型アノテーション周りはかなり仕様が様々です。Pythonのアップデートでもかなり頻繁に機能が追加されたりしています。該当するPEP文書もたくさんあります。その都合で最初から一通り把握した上でベストな状態で型アノテーションしていくというのは中々難しいのでは・・・という感じがしています（この記事を書いている時点で最新のPyhton3.10でも色々型関係のアップデートが入っていますし）。

例えば私は直近になるまで`__future__.annotations`が使えないPythonバージョンでも文字列として型アノテーションすることで（VS Codeなども含め）ちゃんと認識してくれる・・・ということ知りませんでした。普通に自身のクラスのインスタンスの型アノテーションが必要になり且つPython3.6などを最低サポートバージョンにしているライブラリなどではAnyを使ってしまっていました。

参考:

> Python3.7未満では`from __future__ import annotations`を使用できないため、文字列を使用する。
> [Pythonでクラスの引数や戻り値の型アノテーションに自己のクラスを指定する
](https://qiita.com/MtDeity/items/fa6cfc4fff8f58140caa)

他にも本来はProtocolを使うべきCallableのアノテーションをProtocolの存在を知らずに以前はCallableや`# type: ignore`を使ったりなどしてしまっていた時期もあります。

参考:

https://qiita.com/simonritchie/items/2665e1f4d6bad63d652f

ある程度最初のうちから全体感などを把握することは可能だと思いますが、それでも漏れはある程度は出てしまうのはこの仕様の多さなどを加味するとやむを得ない・・・気もしています。その辺で躊躇してしまうよりかは、どんどん型アノテーションを進めていった方が安全性の向上などの面でリターンが大きいのではと判断してあまり気にせずに進めています（新しく知った場合には都度修正や改善を入れていく形でいいかなと）。

# リアルタイムに型のミスやエラーが把握できるのはとても快適

VS Code上のPylanceの話となりますが、プログラミング中にリアルタイムに型のチェックがされたり、null安全的なところなども含めてミスが可視化されるというのはとても開発体験が良く感じています（コンパイル時にエラーで気づく・・・となるよりも私はこちらの方がさくさくと書けて好みです）。

コンパイルが無いという性質上、プロジェクトのコード量が結構多くてもテストなども瞬時にそこだけ通したりも出来ますし、この辺はPython楽で良いなぁ・・・とは思います（もちろん賛否両論あるとは思いますし言語次第なところもありますが、以前使っていた静的型付け言語では数十万行になってくると分割などしないとちょっとビルドなどでの開発体験が悪く感じたり等・・・）。

Dropboxの数百万行といったプロジェクトやJPモルガンの4000万行弱のPythonコードなどと比べると私がお仕事などで扱うコード量は少ない感じではありますが、少なくとも私が今扱っているコード量であればリードタイムなどの指標が悪くなったりといったことは感じていません。良い開発体験を得られていると思います。

※コンパイル時の全体のエラーチェックのようなものは近いものをmypyのチェックをCI等に組み込んだりで、デプロイ前には時間をかけてチェックする・・・みたいなことは実現できます。

# Pylanceなどで一通りチェックが通る状態にするのは慣れてくると案外いける

最初型アノテーションを入れ始めたころにはPylanceなどで結構色々引っかかってしまっており、この辺をちゃんと通すのはいけるのか・・・？と若干懐疑的なところもありましたが、慣れてきたら基本的にチェックに引っかからない形で書けるようになってきました。

Pylanceとmypyでどんな時にどんなエラーが出て、どんな対応をすればチェックが通るのか・・・というのは世の中の記事を読んだだけでは中々身に付けるのは難しく、その辺はとにかく型アノテーションを使い始めて慣れていく必要があるかなとは思います。

# Jupyterなどでも型アノテーションの恩恵を得やすくなった

比較的最近の話なのですが、VS Code上でのJupyterが正式版となりPylanceなどもVS Code上で反映できるようになりました。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">VS CodeのJupyter、Pylanceでの型チェックとかTabnineの補完とか利くようになっている・・・！素敵ですね・・・！<br><br>これで普通のPythonスクリプト扱う感覚で扱えます・・・🎉<a href="https://twitter.com/hashtag/Python?src=hash&amp;ref_src=twsrc%5Etfw">#Python</a> <a href="https://twitter.com/hashtag/Jupyter?src=hash&amp;ref_src=twsrc%5Etfw">#Jupyter</a> <a href="https://t.co/enz3BYpjfs">pic.twitter.com/enz3BYpjfs</a></p>&mdash; simonritchie (@simonritchie_sd) <a href="https://twitter.com/simonritchie_sd/status/1420171784972734464?ref_src=twsrc%5Etfw">July 27, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

今まではJupyterLabなどであれば拡張機能の設定次第で型チェックを反映することもできていましたが、このリリースでぐぐっとVS Code上のJupyterが快適になったように感じています。Jupyterのノートでも積極的に型アノテーションを使っていこうと感じるレベルです。

最近VS Code上のJupyterを触っていらっしゃらない方は是非お試しください。色々改善していてとても快適です。

# ライブラリなどで型チェックを一通り通すのは難しい

例えばPandasとかがそうなのですが、ライブラリ関係がPylanceの型チェックで色々引っかかったりとすることが結構多くあります。この辺は書き方次第で回避ができるものもあればライブラリの都合で対応が難しいケースも結構あります。そういった場合には型チェックを無視するための`# type: ignore`の指定などは結構使ってしまっています（チェックで引っかかる点が残ったままだと他の型エラーなどを見逃しがちになってしまうので、ライブラリ依存でやむを得ない箇所はエラー表示を避ける形で対応したりもしくはラッパー的にインターフェイスを設けて各所でエラーにならないように対応したりしています）。

この辺は「`# type: ignore`を使ってでも型アノテーションを使った方が遥かに安全になる」と判断してあまり気にせずに使っていっています。似たような理由で`Any`の型アノテーションも必要な箇所では普通に躊躇せずに使っています。

# 書籍などでも型アノテーション付きのものが目に付くようになっている

例えば[Classic Computer Science Problems in Python](https://www.amazon.com/Classic-Computer-Science-Problems-Python/dp/1617295981/)などの本もそうですが、書籍のコードがフルに型アノテーションがされている本なども目に付くようになってきました。

そういった書籍を消化した感じ、やはり型アノテーションがあると写経時などにミスに気づきやすかったりコードの把握がしやすかったりとメリットは感じているため、今後もどんどん型アノテーションがされているPython本が増えるといいなと思います。

# 古めのPythonのバージョンをサポートする場合でもtyping_extensionsなどである程度新しい機能を利用することができる

Python 3.8、3.9、3.10と比較的新し目のバージョンで追加になっている機能もtyping_extensionsというPyPI登録してあるパッケージを利用することでPython 3.6や3.7などでも新しい型アノテーションの機能の恩恵を得ることができます。

https://pypi.org/project/typing-extensions/

例えば`TypedDict`や`Protocol`、`TypeGuard`などが利用できるようになります。

参考 :

https://qiita.com/simonritchie/items/63218b0a5c4a3d3632a1

https://qiita.com/simonritchie/items/2665e1f4d6bad63d652f

https://qiita.com/tk0miya/items/1b093c2eee7668fffb62

この記事を書いている時点ですとPython 3.6はまだEoLを迎えていませんし、3.7もEoLは結構先になるため、各ライブラリなども3.6以降をサポート・・・としているケースが多いと思います。あるいは仕事などで使っているPythonが3.6とか3.7といったケースも結構あると思います。

そういった場合でもtyping_extensionsなども使うことで新しい機能などを楽しめますので「Pythonのバージョン古いし型アノテーションを使いだしても微妙なのでは・・・」と感じられている方でも使ってみるのもいいかもしれません。

ただしtypingパッケージの`Dict`や`List`、`Union`などを使わずに`dict[str, int]`や`str|int`といったような新しいPythonでサポートされた書き方はPythonバージョンを上げないと使うことはできません。

# 参考サイトまとめ

https://pypi.org/project/pyright/

https://navi.dropbox.jp/our-journey-to-type-checking-4-million-lines-of-python

https://qiita.com/MtDeity/items/fa6cfc4fff8f58140caa

https://qiita.com/MtDeity/items/fa6cfc4fff8f58140caa

https://qiita.com/simonritchie/items/2665e1f4d6bad63d652f

https://www.amazon.com/Classic-Computer-Science-Problems-Python/dp/1617295981/

https://qiita.com/simonritchie/items/63218b0a5c4a3d3632a1

https://qiita.com/tk0miya/items/1b093c2eee7668fffb62