コメントの目的:「なぜ」を書く

優れたコードコメントの原則は、コードが「何をしているか」ではなく「なぜそうするのか」を補足することです。 コードそのものが「何をしているか」を表現します。コメントが重複して同じことを書くのは価値がありません。

Python — 悪いコメントと良いコメントの対比
# ❌ 悪い例: コードを日本語で繰り返しているだけ
i = i + 1  # i に 1 を加算する

# ✅ 良い例: なぜ +1 するのかを説明している
# DB の行番号は 1 始まりだが、配列インデックスは 0 始まり
# オフセットを合わせるため +1 する
row_number = index + 1

# ❌ 悪い例: 何をするかを繰り返す
# ユーザーを検索する
users = User.find_by(status="active")

# ✅ 良い例: 直感に反する挙動を説明する
# 論理削除済み(deleted_at IS NOT NULL)のユーザーは status="active" には
# 含まれないが、status="suspended" には含まれる点に注意
users = User.find_by(status="active")

コメントで定義すべき内容

コメントの種類書くべき内容
ビジネスロジックの根拠 なぜこの条件・値・計算式なのか 消費税率 10%(2019年10月改正以降)
非自明なアルゴリズム アルゴリズムの概要・計算量・参考文献 // Boyer-Moore 法 O(n/m) → 参考: https://...
ワークアラウンド バグや制約への対処・Issue/PR 番号 # Python 3.11 の asyncio バグ #123 を回避
前提条件・不変条件 引数・状態に対する前提・呼び出し順序の制約 # connect() を呼んだ後でのみ呼び出し可能
パフォーマンス上の意図 なぜこの実装を選んだか # リスト内包表記より 3x 高速(benchmark.py 参照)
危険な副作用 呼び出し側が知るべき破壊的変更 # 引数の list を in-place でソートする
TODO / FIXME 未対応の課題・担当者・期限 # TODO(yamada): 2026-Q3 に削除予定

書いてはいけないコメント

アンチパターンなぜ問題か
コードの繰り返しコードを見れば分かる。変更時にコメントと乖離してリーダーを混乱させる。
コメントアウトされたコードなぜ残っているか不明。git log に任せるべき。
嘘をついているコメントコード変更後にコメントを更新し忘れた場合、コメントを信じると誤解を招く。
長すぎる自明なコメント読む時間が無駄。
個人への批判・感情表現チームの雰囲気を悪化させる。「なぜこんな実装に」はレビューで言う。

⚠️ コメントアウトされたコードは即刻削除する

「後で使うかも」は95%使いません。git log があるので必要なら復元できます。コメントアウトされたコードの放置は技術的負債の典型例です。

Python docstring の形式

Python の公式スタイルガイド PEP 257 では docstring の基本形式を規定しています。プロジェクト内で Google スタイル・NumPy スタイル・Sphinx スタイルのいずれかを統一して使います。

Google スタイル(推奨: 可読性が高い)

Python — Google スタイル docstring
def calculate_discount(price: float, rate: float, *, min_price: float = 0.0) -> float:
    """割引後の価格を計算する。

    消費税(10%)は含まない金額で計算し、割引後が min_price を下回る場合は
    min_price を返す(フロア価格ルール)。

    Args:
        price: 割引前の価格(税抜き、円)。負の値は不可。
        rate: 割引率(0.0〜1.0 の範囲)。0.2 = 20% 引き。
        min_price: 割引後の最低保証価格(デフォルト: 0.0)。

    Returns:
        割引後の価格(税抜き、円)。最低でも min_price 以上の値。

    Raises:
        ValueError: price が負の値の場合、または rate が 0.0〜1.0 の範囲外の場合。

    Examples:
        >>> calculate_discount(1000, 0.2)
        800.0
        >>> calculate_discount(1000, 0.9, min_price=200.0)
        200.0
    """
    if price < 0:
        raise ValueError(f"price は 0 以上の値が必要です: {price}")
    if not 0.0 <= rate <= 1.0:
        raise ValueError(f"rate は 0.0〜1.0 の範囲が必要です: {rate}")
    return max(price * (1 - rate), min_price)

NumPy スタイル(推奨: 数値計算・データサイエンス系)

Python — NumPy スタイル docstring
def moving_average(values: list[float], window: int) -> list[float]:
    """単純移動平均を計算する。

    Parameters
    ----------
    values : list[float]
        時系列データ。空リストは不可。
    window : int
        移動平均のウィンドウサイズ(正の整数)。
        values の長さより大きい場合は ValueError を送出。

    Returns
    -------
    list[float]
        長さ ``len(values) - window + 1`` のリスト。
        各要素は対応するウィンドウ内の平均値。

    Raises
    ------
    ValueError
        values が空の場合、または window が values の長さを超える場合。

    Examples
    --------
    >>> moving_average([1, 2, 3, 4, 5], 3)
    [2.0, 3.0, 4.0]
    """
    if not values:
        raise ValueError("values は空にできません")
    if window > len(values):
        raise ValueError(f"window ({window}) は values の長さ ({len(values)}) を超えられません")
    return [sum(values[i:i + window]) / window for i in range(len(values) - window + 1)]

モジュール・クラスレベルのコメント

Python — モジュール・クラスの docstring テンプレート
"""注文管理モジュール。

注文の作成・更新・キャンセル・照会機能を提供する。
外部サービス連携(決済 API・在庫 API)は order/adapters/ に分離済み。

注意:
    このモジュールは注文テーブルのみを更新する責務を持つ。
    在庫の減算は InventoryService を通じて行うこと(二重減算防止のため)。

Example:
    >>> from order.service import OrderService
    >>> svc = OrderService(db_session)
    >>> order = svc.create(user_id=1, items=[{"product_id": 42, "quantity": 2}])
"""


class OrderService:
    """注文の CRUD と状態遷移を管理するサービスクラス。

    状態遷移:
        pending → confirmed → shipped → delivered
        pending → cancelled(確定前のみ)

    Attributes:
        db (Session): SQLAlchemy セッション。
        inventory (InventoryClient): 在庫 API クライアント。

    Note:
        このクラスはスレッドセーフではない。
        非同期コンテキストでは AsyncOrderService を使用すること。
    """

TODO / FIXME / HACK コメント

課題コメントは形式を統一することで検索・管理が容易になります。

種別用途書き方
TODO 将来対応する必要がある改善・機能追加 # TODO(yamada 2026-Q3): レート制限を追加する
FIXME 既知のバグ・動作しない可能性がある箇所 # FIXME(#241): 1000件超の場合にタイムアウトする
HACK ワークアラウンド・技術的負債 # HACK: 外部ライブラリのバグ #789 を回避するため
NOTE 重要な注意点・落とし穴 # NOTE: この関数は引数の list を in-place でソートする

Python でコメントカバレッジを自動計測する

① interrogate で docstring カバレッジを計測

Python — interrogate で docstring カバレッジを計測
"""
interrogate で関数・クラス・モジュールの docstring カバレッジを計測する。

インストール:
  pip install interrogate

実行:
  interrogate src/ --verbose          # 詳細表示
  interrogate src/ --fail-under 80    # 80% 未満で終了コード 1(CI 用)
  interrogate src/ --badge docs/      # バッジ画像を生成

設定(pyproject.toml):
  [tool.interrogate]
  ignore-init-method = true
  ignore-init-module = false
  ignore-magic = true
  ignore-semiprivate = false
  ignore-private = false
  ignore-property-decorators = false
  ignore-module = false
  fail-under = 80
  verbose = 1
"""

# programmatic に使う場合
import subprocess
import sys


def check_docstring_coverage(path: str, fail_under: int = 80) -> dict:
    """interrogate を実行して docstring カバレッジを返す。"""
    result = subprocess.run(
        ["interrogate", path, f"--fail-under={fail_under}", "--quiet"],
        capture_output=True, text=True
    )
    # 標準出力から数値を抽出
    import re
    m = re.search(r"(\d+\.?\d*)\s*%", result.stdout + result.stderr)
    coverage = float(m.group(1)) if m else 0.0
    return {
        "coverage": coverage,
        "passed": result.returncode == 0,
        "output": result.stdout or result.stderr,
    }

if __name__ == "__main__":
    result = check_docstring_coverage("src/")
    status = "✅" if result["passed"] else "❌"
    print(f"{status} Docstring カバレッジ: {result['coverage']:.1f}%")
    print(result["output"])
    if not result["passed"]:
        sys.exit(1)

② AST で TODO / FIXME コメントを一覧化

Python — ソースコードから TODO/FIXME を一覧抽出
"""
ソースツリーから TODO / FIXME / HACK / NOTE コメントを抽出して一覧表示する。
技術的負債の可視化・定期棚卸しに使用する。
"""
import re
import tokenize
import io
from pathlib import Path

MARKER_PATTERN = re.compile(
    r"(TODO|FIXME|HACK|NOTE)(?:\(([^)]*)\))?:?\s*(.*)",
    re.IGNORECASE
)

def extract_markers(file_path: Path) -> list[dict]:
    """Python ファイルからコメントマーカーを抽出する。"""
    results = []
    try:
        source = file_path.read_text(encoding="utf-8")
        tokens = tokenize.generate_tokens(io.StringIO(source).readline)
        for tok_type, tok_string, (row, _), _, _ in tokens:
            if tok_type != tokenize.COMMENT:
                continue
            # '#' を除去して検索
            comment = tok_string.lstrip("#").strip()
            m = MARKER_PATTERN.search(comment)
            if m:
                results.append({
                    "file": str(file_path),
                    "line": row,
                    "type": m.group(1).upper(),
                    "owner": m.group(2) or "",
                    "message": m.group(3).strip(),
                })
    except (tokenize.TokenError, UnicodeDecodeError):
        pass
    return results

def scan_directory(root: str = ".") -> None:
    """ディレクトリ配下の全 .py ファイルをスキャンする。"""
    all_markers: list[dict] = []
    for path in sorted(Path(root).rglob("*.py")):
        if any(p in path.parts for p in ("venv", ".venv", "__pycache__", ".git")):
            continue
        all_markers.extend(extract_markers(path))

    print(f"{'種別':<8} {'ファイル:行':<50} {'担当者':<15} メッセージ")
    print("-" * 110)
    for m in sorted(all_markers, key=lambda x: (x["type"], x["file"], x["line"])):
        location = f"{m['file']}:{m['line']}"
        print(f"{m['type']:<8} {location:<50} {m['owner']:<15} {m['message']}")
    print(f"\n合計: {len(all_markers)} 件")

if __name__ == "__main__":
    import sys
    scan_directory(sys.argv[1] if len(sys.argv) > 1 else ".")

まとめ

コードコメントで定義すべき要素

なぜを書く(コードの繰り返しではなく理由・意図)
□ 関数・メソッド: docstring(概要・Args・Returns・Raises・Examples)
□ クラス: 責務・状態遷移・スレッドセーフ性・Attributes
□ モジュール: 概要・主要クラス・注意事項・使用例
□ TODO / FIXME: 形式統一(担当者・Issue 番号・期限)
□ ワークアラウンド: バグ番号・回避理由・削除条件
□ 前提条件: 引数の制約・呼び出し順序・スレッド安全性
□ コメントアウトされたコードは即刻削除