コメントの目的:「なぜ」を書く
優れたコードコメントの原則は、コードが「何をしているか」ではなく「なぜそうするのか」を補足することです。 コードそのものが「何をしているか」を表現します。コメントが重複して同じことを書くのは価値がありません。
# ❌ 悪い例: コードを日本語で繰り返しているだけ
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 スタイル(推奨: 可読性が高い)
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 スタイル(推奨: 数値計算・データサイエンス系)
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)]
モジュール・クラスレベルのコメント
"""注文管理モジュール。
注文の作成・更新・キャンセル・照会機能を提供する。
外部サービス連携(決済 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 カバレッジを計測
"""
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 コメントを一覧化
"""
ソースツリーから 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 番号・期限)
□ ワークアラウンド: バグ番号・回避理由・削除条件
□ 前提条件: 引数の制約・呼び出し順序・スレッド安全性
□ コメントアウトされたコードは即刻削除