【2021】モダンなPython開発環境の紹介


# 📌 はじめに

Pythonで開発を行うにあたり、リンタやフォーマッタ、パッケージマネージャ等のツールの選定は非常に重要な問題です。一方で歴史的な経緯もあり、沢山の選択肢から何を選ぶべきか情報がまとまっていないように感じました。この記事では2021年9月時点でモダンと言えるであろう開発環境を紹介します。基本的には**シェアが高い**こと、**著名なパッケージで使用されていること**を主な選定理由としており、また**特定のエディタに依存しない**ことを前提とします。

本記事で紹介する内容は一つのテンプレートに近く、必要に応じてカスタマイズするもよし、そのまま使ってもよし、として参考になればと思います。(CI/CDについてはPythonとは独立した問題なので触れません。またドキュメント生成は`Sphinx`を推しますが、必須ではないので今回は割愛します。)


# 📄 要約

"モダン"な開発環境を箇条で列挙すると下記の通りです。以降、それぞれのツールについて触れます。

- Python
    - CPython: `3.9`
- Tools
    - Package manager: [poetry](https://github.com/python-poetry/poetry)
    - Formatter: [black](https://github.com/psf/black)
        - Import sorter: [isort](https://github.com/PyCQA/isort)
    - Linter: [pflake8](https://github.com/csachs/pyproject-flake8)
      - Plugin: [flake8-bugbear](https://github.com/PyCQA/flake8-bugbear)
    - Type checker: [mypy](https://github.com/python/mypy)
    - Git hook manager: [pre-commit](https://github.com/pre-commit/pre-commit)
- Code format
    - Max line length: `119`

 
<details><summary>オプショナルなツール類</summary><div>

- 複数バージョンのPythonを扱うときなど
    - Test framework: [`tox`](https://github.com/tox-dev/tox)
    - Python version managemer: [`pyenv`](https://github.com/pyenv/pyenv)
- CI/CDの補助
    - Automated dependency updater: [`Dependabot`](https://github.com/dependabot)
    - Code coverage manager: [`codecov`](https://github.com/codecov) (__*__)

(__*__) 2021年4月に不正アクセスを受けていたことを公表しています。

</div></details>




# 🔖 "モダン"な開発環境の概要

便利かつ最新の仕様を反映しているツールを選定するがまず第一です。加えて、**pyproject.tomlに設定類/依存関係を集約すること** が一つのポイントです。`pyproject.toml`は[`PEP518`](https://www.python.org/dev/peps/pep-0518/)にて定義された仕様で、より具体的には従来`requirements.txt`や`setup.py`といったファイルに散乱していた情報を一箇所に集めることができよりスマートになります。リポジトリのルートディレクトリはゴチャゴチャしがちなので、整理されていると嬉しいですよね。

参考
* [PEP518 - Specifying Minimum Build System Requirements for Python Projects](https://www.python.org/dev/peps/pep-0518/)

## ✅ Python: `CPython`: `3.9`

Pythonの実装は様々ありますが、特に理由がなければオリジナルの実装である`CPython`(誤解を恐れずに言えば"普通のPython")を使うのが無難でしょう。

バージョンについては`3.6`系のサポートは`2021-12-23`にEoLを迎えるため、`3.7`系かそれより新しいバージョンを使用するべきです。加えて、パッケージの依存関係等で制約がない限りは基本的に新しいバージョンを使用することをおすすめします。

補足: `3.10`系が`2021-10-04`にリリース予定です。ただしリリース直後は各種パッケージが未対応であることがあるため、しばらくは`3.9`系を使うのが無難かもしれません。

また、オプショナルなツールとして、Pythonのバージョン管理には`pyenv` (≠pyvenv)を使うことをおすすめします。(Node.jsでいうところの`n`に該当します) ※複数のPythonバージョンとパッケージの取り扱い、仮想環境については`PEP582`で議論されていますが、**現時点ではまだDraft**です。

[参考リンク]

* [devguide.python.org/#status-of-python-branches](https://devguide.python.org/#status-of-python-branches)
* [2020年における各Python処理系の状況](https://qiita.com/chromia/items/f2facd0637426b4d06c4)
* [PEP582 - Python local packages directory](https://www.python.org/dev/peps/pep-0582/)


## ✅ パッケージ管理: `Poetry`

https://github.com/python-poetry/poetry

`poetry`はNode.jsでいうところの`npm`や`yarn`、rustでいうところの`cargo`に相当するパッケージマネージャの一つです。比較対象として`pipfile`やrust製で後発の`pyflow`やが挙げられますが、現時点では`poetry`がよいでしょう。これらの比較については既に素敵な記事を書かれてる方がいらっしゃるので参考欄に記載します。

| poetry | pipfile |pyflow |
| :----: | :----: | :----: |
| [![poetry](https://img.shields.io/github/stars/python-poetry/poetry.svg)](https://github.com/python-poetry/poetry) | [![pipfile](https://img.shields.io/github/stars/pypa/pipfile.svg)](https://github.com/pypa/pipfile) | [![pyflow](https://img.shields.io/github/stars/David-OConnor/pyflow.svg)](https://github.com/David-OConnor/pyflow) |

[参考リンク]

* [2020 年の Python パッケージ管理ベストプラクティス](https://qiita.com/sk217/items/43c994640f4843a18dbe)
* [Poetryをサクッと使い始めてみる](https://qiita.com/ksato9700/items/b893cf1db83605898d8a)


## ✅ フォーマッタ: `black`

https://github.com/psf/black

`black`は非常に制約の強いフォーマッタです。裏を返すとコードスタイルに幅がないがゆえにシンプルで、その分の考えるリソースを別の問題に割くことができます。
多くの有名なライブラリにも採用されており、前述の`poetry`や機械学習等で使われる`pandas`なども、`black`によってフォーマットされています。

`black`を使用するにあたっては競合を避けるため、他のツール類に設定が必要な場合があります。本記事で紹介している組み合わせについては各項で取り上げますが、その他については参考欄に公式のDocリンクを貼ります。また、本記事下部の [ max line length: `119` ]についても合わせてご覧ください。

[参考リンク]

* [black#used-by](https://github.com/psf/black#used-by)
* [Pythonのコードフォーマッターについての個人的ベストプラクティス](https://qiita.com/sin9270/items/85e2dab4c0144c79987d)
* [Using Black with other tools](https://black.readthedocs.io/en/stable/guides/using_black_with_other_tools.html)

### ✔️ インポートのソート: `isort`

https://github.com/PyCQA/isort

`isort`は文字通りインポート周りをソート(フォーマット)してくれるツールです。`black`はインポート文の順序までは規定がないため、`isort`と組み合わせて使うことでより快適になります。デフォルトの`isort`の設定では`black`と競合するため、`profile = "black"`として設定を行う必要があります。

## ✅ リンタ: `pflake8`

https://github.com/csachs/pyproject-flake8

`flake8`は最も有名なリンタの一つです。しかし唯一の難点は設定ファイル`.flake8`を`pyproject.toml`に統合できないことです。議論([#issues/234](https://github.com/PyCQA/flake8/issues/234))はされていますが、いくつかの理由で実現に至っていません。

そこで`flake8`をラップし`pyproject.toml`から設定を読むようにしたものが`pflake8`です。正直ここは好みが分かれる部分だと思いますが、本記事ではリポジトリのルートディレクトリの美しさを優先しました。Startsを見てオリジナルの`flake8`を選択するのも良いと思います。

補足1: `black`との競合を避けるため`E203: Whitespace before ':'`は無視する設定を行うことが推奨されています。
補足2: `pflake8`と同様の機能を提供するパッケージは他にも2つ([`flake9`](https://gitlab.com/retnikt/flake9), [`flakehell`](https://github.com/life4/flakehell))見受けられましたが、`pyproject.toml`への設定統合以上の追加機能も提供しており、一般的な開発には過剰と判断しました。

| flake8 | pflake8 |
| :----: | :----: |
| [![flake8](https://img.shields.io/github/stars/PyCQA/flake8.svg)](https://github.com/PyCQA/flake8) | [![pflake8](https://img.shields.io/github/stars/csachs/pyproject-flake8.svg)](https://github.com/csachs/pyproject-flake8) |


[参考リンク]

* [pyproject.toml (PEP 518) support #234](https://github.com/PyCQA/flake8/issues/234)
* [E203: Whitespace before ':'](https://www.flake8rules.com/rules/E203.html)

### ✔️ プラグイン: `flake8-bugbear`

https://github.com/PyCQA/flake8-bugbear

そもそもflake8はコードのエラーチェック、PEP8の遵守チェック、循環的複雑度のチェックをまとめて行ってくれるツールです。これに加えて**バグや問題が起きそうなところの警告**を行うようにしてくれるのが`flake8-bugbear`です。適用したいルールは選択可能なので、一覧を眺めて選ぶもよし、とりあえず全部有効にするもよし、です。

[参考リンク]

* [flake8-bugbear#list-of-warnings](https://github.com/PyCQA/flake8-bugbear#list-of-warnings)

## ✅ タイプチェッカ: `mypy`

https://github.com/python/mypy

Pythonは型を明示することなくコーディングをすることが可能ですが、可読性や保守性を考えると型を明示するべきです。1ファイルに収まる程度の短いスクリプトやNotebookを書くなら別ですが、ある程度の規模のコードを書くならば`Type hints`を添えることの恩恵は大きいです。

`mypy`は静的に型チェックを行うライブラリの一つで、最も古株、そしてベーシックです。Type checkerは現時点で事実上のデファクトスタンダードとなるものがないように思われ、恐らくその要因は用途や目的、そして使用するエディタによって使われているものが異なるためです。(web系なら`pyre`、`vscode`使いなら`pyright`を含む拡張である`pylance`、など)
本記事では最もベーシックかつ依存関係の小さい`mypy`を推奨としますが、参考まで下記に同様の機能を提供するライブラリとの簡潔な比較を記載します。

* [`mypy`](https://github.com/python/mypy)(python.org 製)
    * ✨: 必要十分な機能
    * 🤔: 大規模になると若干遅さを感じる場合がある
* [`pyright`](https://github.com/microsoft/pyright)(Microsoft製)
    * ✨: `mypy`の5倍高速
    * 🤔: インストールに`npm`を要し、前提となる要素が増える
* [`pytype`](https://github.com/google/pytype)(Google製)
    * ✨: 型が明示されていないコードは型推論によって検証する
    * 🤔: 型が明示されていないコードで矛盾がなければ、問題が問題とみなされないことがある
* [`pyre`](https://github.com/facebook/pyre-check)(Facebook製)
    * ✨: Web周りのセキュリティのチェックも行ってくれる(`pysa`の機能)
    * 🤔: インストールに[`watchman`](https://facebook.github.io/watchman/)を要し、前提となる要素が増える

[参考リンク]
    * [4 Python type checkers to keep your code clean](https://www.infoworld.com/article/3575079/4-python-type-checkers-to-keep-your-code-clean.html)
    * [多くのPythonコードに型アノテーションしてみたので色々所感を書いてみる](https://qiita.com/simonritchie/items/fb2d075ef52299ba25e8)

## ✅ Gitフックマネージャ: `pre-commit`

https://github.com/pre-commit/pre-commit

Gitフックを管理するためのフレームワークが`pre-commit`です。`.pre-commit-hooks.yaml`というファイルに設定することで、commit時などにスクリプトが実行されます。スクリプトは自分で書くこともできますが、github上に公開されているものが数多くありますので、それらを指定することで簡単に様々なチクを設定できます。Hookの例を下記に示します。またblackやflake8などのツール類もHookを提供していますので、コミット前にフォーマット、リンティングを自動で行うと安心です。

* repo: https://github.com/pre-commit/pre-commit-hooks
    * hooks:
        * id: trailing-whitespace # 文末の空白を除去
        * id: end-of-file-fixer   # ファイルの末尾が改行か確認
        * id: check-json          # Jsonファイルの形式を確認
        * id: check-yaml          # Yamlファイルの形式を確認


## ✅ コーディング規約

基本的には`black`を使用することで`PEP8`を遵守することとなりますが、`PEP8`には一部モダンではない仕様が含まれています。また`black`も一行の長さ制限についてひとクセあるため明記します。

### ✔️ max line length: `119`

`PEP8`では一行の文字数は`79`文字以内と規定されています。これは諸説ありますが、1行が80文字のエディタ(ディスプレイ)を想定し、勝手に折り返されることを避けることが目的だとされています。(79文字+改行コード=80文字) 

しかし現代のディスプレイサイズを考えればこの`79`という数字は不適切で、あるいは`Type hints`を書こうと思うとあまりに短すぎます。ではいくつに設定するのが良いのか？という問題の一つの答えが`119`です。これはGitHubのCode Review機能で一行に表示される最大の文字数に由来します。

補足: (あまり発生しない事象ですが)`black`は**設定した一行の長さを守ろうと努力しますが、他のルールを破ってまでは守りません**。下記のようなコメントがドキュメントに記載されており、そのために`flake8`の`E501`(line too long)を無効にして`flake8-bugbear`の`B950`(max line lengthを10%超過したら初めて咎める、スピード違反的な取締り方式)を推奨しています。が、上記の通りGitHubのCode Review機能に追従して`119`文字と設定するなら、一文字でも長いと困るので`E501`を使用するのが良いと思います。

> “try to respect --line-length, but don’t become crazy if you can’t”

[blackのドキュメント](https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#line-length)から引用

ところで若干脱線しますが、[blackのドキュメント](https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#line-length)では視覚障害のある方は1行が100文字以上だと見にくいということが言及されています。[W3Cのドキュメント](https://www.w3.org/TR/low-vision-needs/#line-length)においても視覚障害と一行の長さによる見やすさ(見にくさ)は言及されていますので、プロジェクトメンバに合わせて適切な値を設定すると良いと思います。


## 📎 開発環境のサンプル

上記の環境を構成した状態のディレクトリと構築に要したコマンド、そして`pyproject.toml`のサンプルを以下に示します。


```tree.txt
./my-package/
├── .pre-commit-hooks.yaml
├── .venv/
├── README.md
├── my_package/
├── poetry.lock
├── pyproject.toml
└── tests
```

```commands.sh
poetry new my-package
poetry add -D black
poetry add -D pyproject-flake8
poetry add -D flake8-bugbear
poetry add -D isort
poetry add -D mypy
poetry add -D pre-commit
# Add some configurations to pyproject.toml with editor
```

```pyproject.toml
[tool.poetry]
name = "my-package"
version = "0.1.0"
description = ""
authors = ["example <example@example.com>"]

[tool.poetry.dependencies]
python = "^3.9"

[tool.poetry.dev-dependencies]
black = "^21.7b0"
pyproject-flake8 = "^0.0.1-alpha.2"
mypy = "^0.910"
isort = "^5.9.3"
pre-commit = "^2.15.0"
flake8-bugbear = "^21.9.1"

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

# Following configurations are added manually

[tool.flake8]
max-line-length = 119
max-complexity = 10
select = "C,E,F,W,B"
ignore = "E203"

[tool.black]
line-length = 119
exclude = '''
(
    migrations
    | .mypy_cache
    | .pytest_cache
    | .tox
    | .venv
    | dist
)
'''

[tool.mypy]
# common
python_version = 3.9
show_column_numbers	 = true
show_error_context = true
ignore_missing_imports = true
check_untyped_defs = true
disallow_untyped_defs = true
# warning
warn_return_any = true
warn_unused_configs = true
warn_redundant_casts = true

[tool.isort]
profile = "black"
line_length = 119
```