Pythonコードレビューのチェックリスト|Ruff・mypy・pytestで自動化できる項目と人が見るポイント

Pythonのコードレビューは、「機械で見つけられる指摘はツールに任せ、人は設計・動作・テスト・読みやすさを見る」と分けると速く確実になります。具体的には、Ruff(スタイルとバグの芽、セキュリティ)、mypy(型)、pytest(動作)を先に通し、そのうえで下のチェックリストを人が確認します。

この記事では、問題をわざと入れた短いPythonコードを実際にツールにかけ、何が自動で見つかり、何が人のレビューでしか見つからないかを確かめます。チェック項目は、Pythonの公式スタイルガイドPEP 8と、Googleが公開しているコードレビューのガイド「What to look for in a code review」をもとにしています(2026年10月15日確認)。

Pythonコードレビューのチェックリスト

まず全体像です。左の列は「誰が確認するか」で、ツールで確認できるものはレビューの前に自動で通しておきます。

確認する人・ツール観点チェック項目
ツール(Ruff)スタイルPEP 8に沿っているか(インポートは1行に1つ、行の長さ、空白の入れ方など)。フォーマッタで自動整形しているか
ツール(Ruff)バグの芽未使用のインポートや変数、裸の except:、ミュータブルな既定引数がないか
ツール(Ruff のS規則など)セキュリティ文字列を組み立てたSQL、shell=True のコマンド実行、パスワードの直書きがないか
ツール(mypy)型型ヒントと実際の値が食い違っていないか
ツール(pytest)動作テストがあり、すべて通っているか
人設計この変更はこの場所にあるべきか。既存の仕組みとうまく組み合わさっているか
人機能作者の意図どおりに動き、その意図が使う人にとって正しいか。空のリストや0件など境界のケースを考えているか
人複雑さ読む人がすぐ理解できるか。今は必要のない汎用化をしていないか
人テストの中身テストが正しいことを確かめているか。コードが壊れたときに失敗するテストになっているか
人名前とコメント名前から役割がわかるか。コメントが「何をしているか」でなく「なぜそうするか」を説明しているか
人ドキュメント使い方や設定が変わるならREADMEなども更新されているか

「人」の観点は、Googleのガイドの項目(Design、Functionality、Complexity、Tests、Naming、Comments、Style、Documentation)に対応しています。ガイドでは、レビューでいちばん大事なのは変更全体の設計だとしており、スタイルの好みの指摘には「Nit:」を付けて、必須ではない細かい指摘だと伝えることを勧めています。

問題を入れたコードをツールにかけてみる

次のコードには、レビューでよく指摘される問題をいくつか入れてあります。

import os, sys
import sqlite3
import subprocess


def find_orders(conn, customer):
    sql = f"SELECT id, total FROM orders WHERE customer = '{customer}'"
    return conn.execute(sql).fetchall()


def average(values: list[int]) -> int:
    return sum(values) / len(values)


def add_tag(tag, tags=[]):
    tags.append(tag)
    return tags


def load_config(path):
    try:
        with open(path) as f:
            return f.read()
    except:
        return None


def backup(name):
    subprocess.run("cp data.db " + name, shell=True)

確認に使った環境は、Python 3.12.15、Ruff 0.17.0、mypy 2.4.0、pytest 9.1.1です。

pip install ruff mypy pytest

Ruff:スタイル・バグの芽・セキュリティ

Ruffは、Rustで書かれたPythonのリンター兼フォーマッタです。公式ドキュメントによると、Flake8、isort、Blackと同等の機能を1つで提供し、900以上のルールを内蔵しています。ここでは、pycodestyle(E)、Pyflakes(F)、flake8-bugbear(B)、flake8-bandit(S)の4種類のルールを有効にして実行しました。

ruff check --select E,F,B,S --output-format concise orders.py
orders.py:1:1: E401 [*] Multiple imports on one line
orders.py:1:8: F401 [*] `os` imported but unused
orders.py:1:12: F401 [*] `sys` imported but unused
orders.py:2:8: F401 [*] `sqlite3` imported but unused
orders.py:7:11: S608 Possible SQL injection vector through string-based query construction
orders.py:15:23: B006 Do not use mutable data structures for argument defaults
orders.py:24:5: E722 Do not use bare `except`
orders.py:29:5: S602 `subprocess` call with `shell=True` identified, security issue
Found 8 errors.
[*] 4 fixable with `--fix` (1 unsafe fix available with `--fix --unsafe-fixes`).

8件の指摘が出ました。1行に複数のインポート(E401)、未使用のインポート(F401)、文字列で組み立てたSQL(S608)、ミュータブルな既定引数(B006)、裸の except:(E722)、shell=True のコマンド実行(S602)です。このうち4件は --fix で自動修正できると表示されています。

PEP 8は、裸の except: は SystemExit や KeyboardInterrupt まで捕まえてしまい、Ctrl+Cでの中断を難しくし、ほかの問題も隠してしまうと説明しています。SQLについても、Python公式のsqlite3のドキュメントが、SQLインジェクションを避けるために文字列の組み立てではなくプレースホルダーを使うよう求めています。

mypy:型の食い違い

orders.py:12: error: Incompatible return value type (got "float", expected "int")  [return-value]
Found 1 error in 1 file (checked 1 source file)

average は戻り値を int と宣言していますが、割り算の結果は float です。mypyのような型チェッカーは、型ヒントと実際の値の食い違いを実行前に見つけます。型ヒントの書き方はPython list[int]・dict[str, int]とは?で詳しく解説しています。

pytest:動かして初めてわかるバグ

Ruffはミュータブルな既定引数を指摘しましたが、それが実際にどんな不具合になるかは、テストを書くとはっきりします。

from orders import add_tag


def test_add_tag_returns_new_list_each_time():
    assert add_tag("a") == ["a"]
    assert add_tag("b") == ["b"]
>       assert add_tag("b") == ["b"]
E       AssertionError: assert ['a', 'b'] == ['b']
...
FAILED test_orders.py::test_add_tag_returns_new_list_each_time - AssertionErr...
1 failed in 0.01s

2回目の呼び出しで、1回目の "a" が残っていました。既定引数のリストは関数を定義したときに1回だけ作られ、呼び出しのたびに使い回されるためです。

修正後のコードとツールの結果

import shutil
import sqlite3
from pathlib import Path


def find_orders(conn: sqlite3.Connection, customer: str) -> list[tuple[int, int]]:
    sql = "SELECT id, total FROM orders WHERE customer = ?"
    return conn.execute(sql, (customer,)).fetchall()


def average(values: list[int]) -> float:
    if not values:
        raise ValueError("values is empty")
    return sum(values) / len(values)


def add_tag(tag: str, tags: list[str] | None = None) -> list[str]:
    tags = [] if tags is None else tags
    tags.append(tag)
    return tags


def load_config(path: Path) -> str | None:
    try:
        return path.read_text(encoding="utf-8")
    except FileNotFoundError:
        return None


def backup(name: str) -> None:
    shutil.copy("data.db", name)

テストには、SQLインジェクションを試す入力を渡しても値として扱われることの確認を足しました。

import sqlite3

from orders import add_tag, find_orders


def test_add_tag_returns_new_list_each_time():
    assert add_tag("a") == ["a"]
    assert add_tag("b") == ["b"]


def test_find_orders_treats_input_as_value():
    conn = sqlite3.connect(":memory:")
    conn.execute("CREATE TABLE orders (id INTEGER, customer TEXT, total INTEGER)")
    conn.executemany("INSERT INTO orders VALUES (?, ?, ?)",
                     [(1, "sato", 1200), (2, "suzuki", 800)])
    assert find_orders(conn, "sato") == [(1, 1200)]
    assert find_orders(conn, "x' OR '1'='1") == []
$ ruff check --select E,F,B,S --output-format concise orders.py
All checks passed!
$ mypy orders.py
Success: no issues found in 1 source file
$ pytest -q
..                                                                       [100%]
2 passed in 0.01s

3つのツールがすべて通りました。ここまでをCI(GitHub Actionsなど)でプルリクエストのたびに自動で実行するようにしておけば、レビューする人はスタイルや未使用のインポートを指摘する必要がなくなり、次の「人が見る部分」に集中できます。

ツールでは見つからない:人が見るポイント

上の例では、ツールが見つけられなかった問題もあります。average に空のリストを渡すと ZeroDivisionError になる点は、型もスタイルも正しいので、どのツールも指摘しませんでした。修正版では空のときに明示的な例外を出すようにしましたが、「空のときに例外にするのか、0を返すのか」は仕様の判断で、人が決めることです。

  • 仕様と合っているか:プルリクエストの説明に書かれた目的と、コードの動きが一致しているか。境界の値(空、0、最大値、日付の切れ目)を考えているか
  • 変更の大きさ:1つのプルリクエストに複数の目的が混ざっていないか。Googleのガイド「Small CLs」も、小さな変更のほうが速く、丁寧にレビューできると説明しています
  • テストの質:テストが通ることだけでなく、コードが壊れたときに失敗するテストになっているか
  • 例外の扱い:握りつぶさず、呼び出し元が対処できる例外を、意味のあるメッセージで出しているか
  • 秘密情報:APIキーやパスワードがコードやログに出ていないか。設定は環境変数などに分けているか
  • 読みやすさ:初めて読む人がすぐ理解できるか。理解に時間がかかるなら、名前や関数の分け方を見直す

入力値の検証や安全な書き方をさらに詳しく知りたい場合はセキュアコーディングとは?、レビューを含むチーム開発の進め方はチーム開発の進め方、AIによる自動レビューはCodeRabbitとは?で解説しています。

レビューする側・される側のコツ

立場コツ
レビューする側まず全体の設計を見てから細部へ進む。指摘には理由を添え、必須か任意(Nit)かを区別する。良い点もコメントする
レビューされる側変更を小さく分け、目的と確認方法を説明に書く。出す前にツールを通し、自分で一度差分を読み返す

よくある質問

Q. Pythonのコードレビューでは何をチェックすればいいですか?

スタイル、未使用のコード、危険な書き方、型の食い違い、テストの合否はツール(Ruff、mypy、pytest)で確認し、人は設計、仕様どおりの動作、複雑さ、テストの中身、名前とコメント、ドキュメントの更新を確認します。

Q. flake8とBlackの代わりにRuffを使ってもいいですか?

Ruffの公式ドキュメントでは、Flake8、isort、Blackと同等の機能を提供すると説明されています。新しく始めるプロジェクトなら、リンターとフォーマッタをRuffにまとめると設定が1つで済みます。既存のプロジェクトでは、ルールの差を確認してから切り替えてください。

Q. 裸の except: はなぜ指摘されるのですか?

PEP 8によると、裸の except: は SystemExit や KeyboardInterrupt まで捕まえてしまい、Ctrl+Cでの中断を難しくし、ほかの問題も隠してしまうためです。捕まえたい例外を具体的に書きます。

Q. ツールが全部通ればレビューは不要ですか?

不要にはなりません。この記事の例でも、空のリストで割り算がエラーになる問題はどのツールも指摘しませんでした。仕様に合っているか、境界のケースを考えているかは人が確認する必要があります。

まとめ

Pythonのコードレビューは、Ruff・mypy・pytestで機械的に確認できることを先に自動化し、人は設計、仕様との一致、境界のケース、テストの質、読みやすさに時間を使うのが効率的です。この記事のチェックリストをプルリクエストのテンプレートに入れ、ツールの実行をCIに組み込むところから始めてみてください。

あわせて読みたい

学習の順序はPython学習のロードマップ、標準で使える機能の一覧はPython標準ライブラリのモジュール一覧にまとめています。

開発環境とGitの全体像はGitと開発環境の基礎にまとめています。

プログラミング学習に興味がある方は、まずは無料体験会へ

無料体験会・お申し込みはこちら