Pythonのコードフォーマッターについての個人的ベストプラクティス

Pythonには多くのlinter・formatterが存在していて、どれを使えばいいか迷います。そこで有名どころをいくつか試してみて、個人的に運用がしやすいと思った組み合わせを考えました。

# linterとformatter
linterとformatterはどちらもプログラムの実行を伴わない静的解析ツールですが、それぞれ役割が違います。

この二つについて、当初勘違いをしていました。
コードがある基準に従って書かれているかをチェックするためのツールがlinter、基準に従うように自動修正するのがformatterだと思っていたのですが、少し違うようです。

linterとは、対象のコードがベストプラクティスに沿っているかどうかをチェックするツールです。例えばPythonで言うと、boolean値は`==`ではなく`is`で比較するとか、型の比較には`type()`ではなく`isinstance()`を使うとか、ラムダ式は変数に代入しない等、プログラムの動作上問題はないけれど良いコードにするために推奨されている書き方というのがあります。これらの書き方に合致しているかどうかをチェックするのがlinterです。またlinterによっては、deplicatedの関数の使用していないか、未使用・重複している変数がないかといった、実行時エラーにはならないが潜在的なエラーの原因になる箇所をチェックしてくれます。
基本的には問題箇所を警告するのがlinterの役割ですが、オプションで自動修正する機能を持つものもあります。ただし、これらの箇所の変更はプログラムの動作に影響を与えるため、自動修正は慎重に行う必要があります。

これに対しformatterは、コードのスタイルをチェックするツールです。コードのスタイルとは、スペースの数とか、改行の位置とか、コメントの書き方といった、プログラムの動作ではなく見た目に関わることです。formatterで自動修正を適用しても、通常はプログラムの動作には影響しません。
linterとは逆に、formatterは検出したエラーの自動修正が主目的ですが、エラーを警告するだけに留めることも可能です。

以上がlinterとformatterについての自分の理解です。…が、lintがチェック、formatが自動修正という分類も多くの場合は間違っていないので、この記事ではその意味でlinterとformatterという言葉を使い分けることにします。

linterとformatterでは役割が異なりますが、スタイルに関わる部分など、一部対象が被ることもあります。Pythonのlinterとformatterを完全に兼ね備えたツールは現状では存在しないため、それぞれ別々のツールを使う必要があります。

# Pythonのコーディング規約
Pythonには[PEP8](https://www.python.org/dev/peps/pep-0008/)という、公式コミュニティによって定められたコーディング規約が存在します。

PEP(Python Enhancement Proposal)はPythonの仕様についての提案をまとめた文書群です。その8番目の文書が`PEP 8 – Style Guide for Python Code`で、Pythonの推奨される書き方についてまとめられています。見てみると分かりますが、lintとformatの両方のルールが定められています。

Pythonの多くのlinterやformatterは、基本的にこのPEP8に沿ってコードチェックを行います。

# 各ツールの比較
## linter比較
Pythonのlinterにはいくつかありますが、有名なのはflake8とpylintの二つです。

### pycodestyle
https://github.com/PyCQA/pycodestyle

pycodestyleは、PythonのコードがPEP8に準拠しているかをチェックするためのlinterです。以前はpep8という名前でしたが、文書の方のPEP8と名前が同じでややこしいので、pycodestyleという名前に改名されました。
コードスタイルについてのチェックも行うため、前述の厳密な分類によればformatterとしての側面も持つことになります。ただし、自動修正はできません。

シンプルなlinterですが、後述のflake8に内包されているため、現在では単体でこれを使うことはあまりないと思われます。

### pyflakes
https://github.com/PyCQA/pyflakes

こちらもlinterの一つですが、pycodestyleとは違い、コードスタイルについては一切チェックせず、未使用のimport文や変数などの、pycodestyleには検出できない論理的なエラーのみを検出します。

こちらも自動修正機能はなく、自動修正には後述のautoflakeを使います。

### flake8
https://github.com/PyCQA/flake8

flake8は、以下の3つのlintツールをまとめたラッパーです。

- pycodestyle
- pyflakes
- mccabe

pycodestyleとpyflakesについては前述の通りです。
mccabeは循環的複雑度というコードの複雑度をチェックできるツールですが、flake8のデフォルトでは無効になっています。私の周りでも、mccabeが使われているところはあまり見たことがありません。ちなみに開発者のThomas McCabeさんの名前に由来しており、マッケイブと読みます。

このようにflake8は複数のコードチェックに対応した汎用的なlintツールで、この記事が書かれている時点で最も広く使われているlinterと言っていいと思います。

### pylint
https://github.com/PyCQA/pylint

pylintもPythonの汎用的なlinterの一つで、VSCodeのデフォルトのlinterはこれが使われています。オプションが豊富で多くのチェック項目に対応できるのが特徴です。flake8のライバル候補ですが、最近はflake8の方が支持されているようです。

## formatter比較
flake8やpylintはコードスタイルのチェックも行ってくれるため、厳密に言えばformatterとしての側面も持ちますが、自動修正まではしてくれません。エラーを自動で修正したい場合は、別途コードフォーマットツールを使う必要があります。

formatterにもいくつか種類がありますが、現実的な選択肢として候補に上がるのは、autopep8とyapf、そしてblackの三つでしょうか。

### autopep8
https://github.com/hhatto/autopep8

autopep8は、その名の通りPEP8に準拠するようにコードを整形するツールです。pycodestyleの検出したエラーを修正してくれますが、全てのエラーに対応しているわけではありません。([参考](https://github.com/hhatto/autopep8#features))

Pythonのコードフォーマッターとしては古株で、特別悪い点もないのですが、最近はあまり使われていないようです。個人的には、過剰な改行を修正してくれない点が不満です。

ちなみに作者は日本人の方。

### yapf
https://github.com/google/yapf

googleが開発しているコードフォーマッターです。Yet Another Python Formatterの略だそうです。(yapfは何と発音するんでしょうか)

カスタマイズ性が非常に高く、ベースのコードスタイルをpycodestyleやgoogle独自のスタイルから選ぶことができる他、多くの項目をオプションで設定できます。

天下のgoogleが開発していることもあり安心感は抜群で、機能も十分なのですが、最近はblackの人気に押されている印象です。

### black
https://github.com/psf/black

比較的最近登場した、新進気鋭のコードフォーマッター。
他のフォーマッターよりも制限が厳しく、設定できるオプションがかなり少ないことが特徴です。シングルクォートとダブルクォートの統一など、pycodestyleでは対応していないが不揃いだと気になりがちな、細かなスタイルまで統一してくれます。

カスタマイズ性が低い点がPythonerに好まれているのか、最近では非常に人気があります。Django・pandas・Poetryなど多くの有名プロジェクトでも採用されており、現在最も勢いのあるフォーマットツールです。

## その他コード整形ツール
他のlinterやformatterが対応していないが、コードを綺麗に保つために有用なツールです。

### autoflake
https://github.com/myint/autoflake

autoflakeはpyflakesが検出したエラーを自動修正してくれるツールで、未使用のインポート文や変数を削除してくれます。

他のlinter・formatterにはこの機能はないため、このエラーを自動で修正したい場合はautoflakeを使う必要があります。

### isort
https://github.com/PyCQA/isort

isortはimport文の並び順をチェックするツールです。linterの一種ですが、用途が限定的なのでこちらに書いています。並び順の自動修正も可能で、import文を標準ライブラリ→ローカルファイルの順、かつアルファベット順に並べ替えてくれます。

import文の順番についてはPEP8に規定がありますが、自動修正に対応しているツールが他にないため、import文の並び替えたいをしたい場合はisortが必要になります。

## どのツールを使うか
どのツールを使うかは、現在のトレンド・対応項目の網羅性・運用の楽さを考慮して決めます。

### lint
ソースコードのlint全般については、基本的にflake8を使い、flake8よりも細かいスタイルのチェックにはblackを使うのが良いです。さらにimport文の並び順のチェックにisortを使います。

import文の並び順はどうでもいいと言えばどうでもいいのですが、自分は結構気にするのでisortを使っています。import文が綺麗に並んでいるのは気持ちが良いです。

### format
ソースコードのformatについては、メインにblack、import文の並び替えにisortを使います。
autoflakeは使っても良いのですが、未使用のインポート文・変数を削除するためだけにツールが増えるのはやや渋いので、個人的にはなくてもいいと思います。自動修正しなくても、flake8が警告してくれるので、手動で削除すれば十分です。

## 各ツールの設定
### black
blackはほとんどオプションがないので、そもそも考える余地が少ないです。迷うとしたら、一行の文字数と、シングル・ダブルクォーテーションの統一くらいでしょうか。

一行の文字数はデフォルトだと88文字になっていますが、もう少し長くすることをお勧めします。周りのプロジェクトやOSSでは、119文字に設定しているところをよく見ます。119文字というのは、githubのコードレビュー画面の幅と同じだかららしいです。

シングルクォーテーションとダブルクォーテーションの使い分けについてはPEP8では特に規定されておらず、ただ「一貫せよ」とだけ書いてあります。blackではダブルクォーテーションに統一されます。二つはPythonでは全く同一ですが、慣習的に使い分けている人もいて反対意見も根強いためか、blackにしては珍しくオプションで無効化することができます。自分は統一する派です。なお、シングルクォーテーションに統一することはできません。

blackの設定は、コマンドラインのオプションで直接指定するか、`pyproject.toml`で設定できます。自分はよくパッケージ管理にpoetryを使うので、poetryの設定と一緒に`pyproject.toml`に書くことが多いです。

```
[tool.black]
line-length = 119
```

コマンドラインのオプションで指定する場合は、`black --line-length 119 target_file_or_directory`のようにします。blackはオプションが少ないので、他で`pyproject.toml`を使っていなければコマンドラインで指定する方がシンプルだと思います。

クォーテーションの統一を無効化したい場合は`pyproject.toml`に`skip-string-normalization = true`の設定を追加します。コマンドラインの場合は`--skip-string-normalization`を付けます。

### flake8
flake8のデフォルトのルールには一部blackと競合するものがあるので、それらをblack側に合わせる必要があります。

競合を回避するための設定をblack公式が公開してくれているので([参考](https://github.com/psf/black/blob/master/docs/compatible_configs/flake8/.flake8))、基本はその通りにします。flake8のデフォルトの一行の文字数は79文字なので、blackに設定した文字数で上書きします。

flake8の設定は`.flake8`、`setup.cfg`、`tox.ini`に書くことができます。自分は`setup.cfg`に書いています。

```
[flake8]
extend-ignore = E203
max-line-length = 119
```

### isort
isortのデフォルトのルールもblackといくつか競合します。

こちらもblack公式が対応箇所をまとめてくれているので、それをそのまま使います([参考](https://github.com/psf/black/blob/6d0bdc2f38d57ddf07e7d127215d51f8553997f9/docs/compatible_configs.md#isort))。isortのデフォルトの一行の文字数も79文字なので、blackに合わせます。

isortの設定は`.isort.cfg`、`pyproject.toml`、`setup.cfg`、`tox.ini`、`.editorconfig`に書くことができます。例えば`pyproject.toml`に書く場合はこうなります。

```
[tool.isort]
multi_line_output = 3
include_trailing_comma = true
force_grid_wrap = 0
use_parentheses = true
ensure_newline_before_comments = true
line_length = 119
```

もしくはisortのバージョンが5以上の場合は、`profile = black`とするだけで、isort公式が用意してくれている上記の設定を利用できます。設定がとてもシンプルになるのでおすすめです。

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

flake8とisortの設定ファイルは、他に使っているツールも考慮して、なるべく少ないファイルにまとめられるように選びます。自分はblackとisortの設定を`pyproject.toml`に、flake8の設定を`setup.cfg`に書いています。flake8が`pyproject.toml`に対応してくれれば一つにまとめられるのですが。。

# 定期実行の方法
当然ですが、linterもformatterも設定しただけでは意味はなく、定期的に実行する必要があります。
定期実行する方法はいくつか考えられます。

## 直接コマンドを実行する
定期実行ではありませんが、ターミナルから直接lint・formatコマンドを実行することもよくあります。

lint・formatのためには複数のコマンドを実行する必要があるので、Makefileやシェルスクリプトを作ってワンライナーで実行できるようにしておくと便利です。

例えば自分は次のようなMakefileを作っています。

```
lint:
	flake8 target_file_or_directory
	isort --check --diff target_file_or_directory
	black --check target_file_or_directory

format:
	isort target_file_or_directory
	black target_file_or_directory
```

このようなMakefileを作っておけば、`make lint`でlintを、`make format`でformatを実行できます。
ただし、windowsではデフォルトではmakeは使えません。

## コードエディタと連携する
自分はVSCodeを使っているので他のエディタについては分かりませんが、VSCodeはPythonの自動lintingや保存時の自動修正に対応しています。

連携するためには`.vscode/settings.json`に以下の内容を設定します。
VSCodeではデフォルトのlinterとしてpylintが使われるので、それを無効化し代わりにflake8を有効にします。さらにformatterとしてblackとisortを使うように設定しています。autoflakeのフォーマットには対応していないようです。

```json
{
    "python.linting.enabled": true,
    "python.linting.pylintEnabled": false,
    "python.linting.flake8Enabled": true,
    "python.linting.lintOnSave": true,
    "python.formatting.provider": "black",
    "python.formatting.blackArgs": [
        "--line-length",
        "119"
    ],
    "editor.formatOnSave": true,
    "editor.codeActionsOnSave": {
        "source.organizeImports": true
    }
}
```

blackの設定を別ファイルに書いている場合は`python.formatting.blackArgs`は不要です。

`editor.formatOnSave: true`にすると保存時の自動スタイルフォーマットが、`source.organizeImports: true`にすると保存時にimport文の自動ソートが有効になります。ただ、自分の予期しないタイミングでコードに変更が入るのが好きではないので、ファイル保存時の自動フォーマットは自分は使っていません。

## Git Hooksを利用する
gitにはhooksという、commitやpushなどの特定のアクションの前後にあらかじめ設定したスクリプトを自動で実行する仕組みがあります。この仕組みを使って、例えばコミットの直前にlintをかけるようにすれば、lintに失敗した場合はcommitが作成できなくなるため、フォーマットし忘れることがなくなります。

コミットの直前にスクリプトを実行したい場合は`.git/hooks/pre-commit`にそのスクリプトを書けばいいのですが、`.git/`ディレクトリ以下のファイルはgitの管理対象外なため、チーム間で共有することができません。そこで、`pre-commit`というPythonのツールを使います。

`pre-commit`は名前の通り、pre-commitのhooksの設定を簡単にしてくれるツールです。`pip install pre-commit`でインストールできます。このツールを使うと、`.pre-commit-config.yaml`という名前のファイルに設定を記述し、`pre-commit install`を実行することで、yamlファイルの内容に沿った`.git/hooks/pre-commit`を作成してくれます。

例えばコミットの直前にlintしたい場合、`.pre-commit-config.yaml`は次のようになります。

```
repos:
  - repo: local
    hooks:
      - id: lint
        name: lint
        entry: bash -c 'flake8 target_file_or_directory && isort -c --diff target_file_or_directory && black --check target_file_or_directory'
        language: system
        types: [python]
```

Makefileを導入している場合は、代わりにmakeコマンドを使うこともできます。

```
repos:
  - repo: local
    hooks:
      - id: lint
        name: lint
        entry: bash -c 'make lint'
        language: system
        types: [python]
```

この`.pre-commit-config.yaml`をコミットすることで、hooksの設定を共有することができるようになります。

コミット前のタイミングでlintだけでなくformatをかけることも可能ですが、予期しない変更がコミットに入り込む可能性があるので、lintに留めておくのが良いです。

## CIツールを利用する
ローカルではなくサーバーサイドでlintを実行する方法です。CircleCIやGitHub ActionsなどのCIツールを使ってコミットのpush時にlintをかけることで、formatのし忘れに気づくことができます。

GitHub Actionsのサンプルはこんな感じです。lintに失敗した場合はエラーになりその後のstepは実行されないため、formatし忘れたコードがビルドされたりmergeされてしまうことを防止できます。

```
name: Code Format Sample

on: [push]

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v2
    - name: Set up Python 3.9
      uses: actions/setup-python@v2
      with:
        python-version: 3.9
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install flake8 isort black
    - name: Lint
      run: |
        flake8 target_file_or_directory
        isort --check --diff target_file_or_directory
        black --check --line-length 119 target_file_or_directory
```

こちらもlintだけでなくformatをかけることも可能ですが、Git Hooksと同じ理由でお勧めはしません。

Git Hooksによるコミット前のlintとCIツールによるpush時のlintはタイミングが被るため、両方でlintするのはやや過剰で、どちらか一方を使えば十分だと思います。

## 実際の運用方法
まず、前述した通りに、Makefileの作成・VSCodeのlinter連携・Github Actionsの設定をしておきます。

開発をする際は、コミット直前に`make format`でフォーマットを実行します。その上でVSCodeのlintエラーが出ていないか確認し、エラーが出ていれば手動で修正します。もしフォーマッターを適用し忘れたとしても、Github Actionsの方でエラーになるので、マージされる前に気付くことができます。