Claude CodeでPythonのdocstringを補う — interrogateで未記載の関数だけを書かせる
interrogateのdocstringカバレッジを完了条件にして、Claude Codeに未記載の公開関数だけを書かせる手順です。実装と食い違う説明を防ぐ照合と、Stop hookの例付きです。
Claude CodeにPythonのdocstringを補わせるなら、interrogateの --fail-under を完了条件にし、未記載の関数だけを対象にするのが手堅い構成です。interrogateはコードベースを走査して、モジュール・クラス・関数・メソッドのどれにdocstringがあり、どれに無いかを数えるツールです。数値と一覧が出るので、「どこまで書けば終わりか」をClaude Codeに機械的に伝えられます。
ただし、数えるのは有無だけです。出力は COVERED と MISSED の2値で、書かれた説明が実装と合っているかは見ません。カバレッジを100%にするだけなら、Claude Codeは本体を読まずに定型文を並べても通ってしまいます。そこでこの記事では、計測の設定、Claude Codeへの指示、実装と食い違う説明を弾く照合、Stop hookでの強制までを1本の流れで組みます。
試した環境はPython 3.12.2とinterrogate 1.7.0です。
interrogateが数える対象を先に決める
interrogateは pip install interrogate で入り、interrogate [PATH] で動きます。しきい値の --fail-under は既定が80.0で、カバレッジがこれを下回ると失敗します。Python 3.8以上が対象です。
数える対象は設定で絞れます。「未記載の公開関数だけ」を作るには、次のオプションが効きます。
| オプション | 外れるもの | 注意点 |
|---|---|---|
-s / --ignore-semiprivate | 外れるもの先頭が _ 1つの名前 | 注意点_helper のような内部関数 |
-p / --ignore-private | 外れるもの先頭が __ 2つの名前 | 注意点_ 1つは対象に残る |
-m / --ignore-magic | 外れるもの__repr__ などの特殊メソッド | 注意点__init__ は含まれない |
-i / --ignore-init-method | 外れるものクラスの __init__ | 注意点-m とは別に指定する |
-I / --ignore-init-module | 外れるもの__init__.py のモジュール | 注意点空の __init__.py が多いとき |
-n / --ignore-nested-functions | 外れるもの入れ子の関数・メソッド | 注意点クロージャが多いコード向け |
見落としやすいのは、-p と -s の違いです。Pythonで内部用を示す慣習は _name の単一アンダースコアですが、-p が外すのは __name の二重アンダースコアだけです。-m も __init__ を外しません。次の invoice.py で ignore-private と ignore-magic だけを有効にして走らせると、この差がそのまま出ました。
"""請求書の計算。"""
from __future__ import annotations
def calc_total(items: list[tuple[int, int]], tax_rate: float = 0.1) -> int:
"""明細の合計を返す。"""
subtotal = sum(price * qty for price, qty in items)
if subtotal < 0:
raise ValueError("negative subtotal")
return int(subtotal * (1 + tax_rate))
def apply_discount(total: int, rate: float) -> int:
if not 0 <= rate <= 1:
raise ValueError("rate out of range")
return int(total * (1 - rate))
def _round_yen(value: float) -> int:
return int(round(value))
class Invoice:
def __init__(self, items):
self.items = items
def total(self) -> int:
return calc_total(self.items)
def __repr__(self):
return f"Invoice({self.items!r})"interrogate -c pyproject.toml src| invoice.py (module) | COVERED |
| calc_total (L5) | COVERED |
| apply_discount (L13) | MISSED |
| _round_yen (L19) | MISSED |
| Invoice (L23) | MISSED |
| Invoice.__init__ (L24) | MISSED |
| Invoice.total (L27) | MISSED |
--------------- RESULT: FAILED (minimum: 100.0%, actual: 28.6%) ----------------__repr__ は ignore-magic で外れましたが、_round_yen と Invoice.__init__ は残っています。公開APIだけを対象にしたいなら、ignore-semiprivate と ignore-init-method を足します。この2つを足すと、同じファイルの未記載は apply_discount・Invoice・Invoice.total の3件になりました。
pyproject.toml には次のように書きます。interrogateは pyproject.toml を自動で見つけ、コマンドラインオプションの既定値として読みます。
[tool.interrogate]
ignore-init-module = true
ignore-init-method = true
ignore-semiprivate = true
ignore-private = true
ignore-magic = true
fail-under = 100
exclude = ["tests", "docs", "build"]
verbose = 2verbose = 2 は -vv と同じで、関数ごとの COVERED / MISSED と行番号が出ます。Claude Codeに渡す出力はこの形が向いています。MISSED の行を拾えば、直すべき関数と行が一度に分かるからです。
Claude Codeに渡す指示をCLAUDE.mdに固定する
対象の線引きは pyproject.toml に任せ、書き方の規約はCLAUDE.mdに置きます。毎回プロンプトで伝えるより、セッションをまたいで同じ規約が効きます。
## Pythonのdocstring
- docstringが無い公開関数・クラスにだけ追加する。既に書かれたdocstringは書き換えない
- 書く前に、関数本体・型ヒント・`raise` を読む。読んで分かることだけを書く
- `Args` は引数名と型ヒントに一致させる。`Raises` は本体にある `raise` の例外型だけを書く
- 本体から読み取れない仕様(単位、呼び出し側の前提)は推測で書かず、TODOとして報告する
- docstring以外のコードは変更しない
- 作業後に `interrogate -c pyproject.toml src` を実行し、`MISSED` が0であることを示すポイントは3つあります。
- 「既に書かれたものは書き換えない」で、手書きの良い説明を平凡な文に置き換える事故を避けます
- 「読んで分かることだけ」で、関数名から想像した説明を封じます
- 「TODOとして報告する」で、書けない関数を空欄のまま通さず、人が決める箇所として残します
依頼の文面は短くて足ります。
interrogateで MISSED になっている関数に docstring を追加して。
CLAUDE.md の docstring 規約に従い、docstring 以外のコードは触らないで。実装と食い違う説明を防ぐ3つの照合
カバレッジの数字は有無しか見ないため、内容の正しさは別の確認で担保します。Claude Codeに自己申告させるだけでなく、機械で確かめられるものは機械に回します。
docstringを足した後に確かめる順序
- 1
コードが変わっていないことを確かめる
docstringを足す作業で、本体のロジックが変わっていないかを先に見ます。次の節のスクリプトが使えます。
- 2
型ヒントと引数を突き合わせる
Argsの名前と型が、シグネチャに書かれた引数名・型ヒントと一致しているかを、差分で読みます。 - 3
例外を突き合わせる
Raisesに書かれた例外型が、本体のraiseと一致しているかを見ます。書かれていない例外が本体にあれば、その漏れも拾います。
docstring以外の変更をASTで検出する
「docstringだけ足して」と頼んでも、Claude Codeが近くのコードを整えてしまうことはあります。git diff を目で追う手もありますが、AST(抽象構文木)を比べれば機械で判定できます。docstringを取り除いたASTがHEADと一致すれば、変更はdocstringだけです。
"""docstring以外の差分が無いことをASTで確かめる。"""
import ast
import subprocess
import sys
def strip_docstrings(source: str) -> str:
tree = ast.parse(source)
for node in ast.walk(tree):
if isinstance(node, (ast.Module, ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)):
body = node.body
if body and isinstance(body[0], ast.Expr) and isinstance(
getattr(body[0], "value", None), ast.Constant
) and isinstance(body[0].value.value, str):
node.body = body[1:] or [ast.Pass()]
return ast.dump(tree)
def main(path: str) -> int:
old = subprocess.run(
["git", "show", f"HEAD:{path}"], capture_output=True, text=True, check=True
).stdout
new = open(path, encoding="utf-8").read()
if strip_docstrings(old) != strip_docstrings(new):
print(f"NG: {path} にdocstring以外の変更があります", file=sys.stderr)
return 1
print(f"OK: {path} はdocstringだけの変更です")
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1]))先ほどの apply_discount にdocstringを足しただけの状態で走らせると、OK で終了コード0になりました。続けて本体の return int(total * (1 - rate)) の末尾に + 1 を加えると、NG を出して終了コード1になりました。docstringの追加とロジックの変更を、ファイルごとに切り分けられます。
型ヒントと例外は差分で読ませる
引数と例外の照合は、機械化するより読み方を決めるほうが現実的です。Claude Codeには、書いたdocstringの横に根拠を示させます。
追加した各docstringについて、Args・Raises・Returnsの各項目が
関数本体のどの行に対応するかを表で示して。対応する行が無い項目は削って。たとえば apply_discount なら、次のような形に収まるはずです(docstringの書き方の例であり、Claude Codeの実際の出力ではありません)。
def apply_discount(total: int, rate: float) -> int:
"""割引率 rate を適用した合計を返す。
Args:
total: 割引前の合計(円)。
rate: 割引率。0以上1以下。
Raises:
ValueError: rate が0〜1の範囲外のとき。
"""
if not 0 <= rate <= 1:
raise ValueError("rate out of range")
return int(total * (1 - rate))Raises の ValueError は本体の raise ValueError(...) に、rate の範囲は 0 <= rate <= 1 の条件に対応します。対応する行が示せない項目が出たら、それが推測で書かれた部分です。
Stop hookでカバレッジを完了条件にする
頼むたびに「interrogateを実行して」と書かなくても、Stop hookで強制できます。Stop はClaudeが応答を終えるときに発火し、終了コード2で止めると「Claudeが止まるのを防ぎ、会話を続けさせる」挙動になります。stderrに書いた内容は、続行の理由としてClaudeに渡ります。
#!/bin/bash
INPUT=$(cat)
# すでにStop hook経由で続行中なら止まらせる(無限ループ防止)
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
exit 0
fi
cd "${CLAUDE_PROJECT_DIR:-.}" || exit 0
if ! command -v interrogate >/dev/null 2>&1; then
echo "interrogateが見つかりません。PATHか仮想環境を確認してください。" >&2
exit 0
fi
REPORT=$(interrogate -c pyproject.toml src 2>&1)
STATUS=$?
if [ "$STATUS" -ne 0 ]; then
echo "docstringカバレッジが下限を下回っています。MISSEDの関数だけ補ってください:" >&2
echo "$REPORT" | grep MISSED >&2
exit 2
fi
exit 0これを .claude/hooks/docstring-gate.sh に保存して chmod +x し、.claude/settings.json に登録します。
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/docstring-gate.sh"
}
]
}
]
}
}標準入力に {"stop_hook_active": false} を渡して手元で動かすと、先ほどの invoice.py では MISSED の行がstderrに出て終了コード2になりました。{"stop_hook_active": true} を渡すと何もせず終了コード0です。実セッションの前にこの2通りを試しておくと、hookの配線ミスを切り分けやすくなります。
stop_hook_active の確認は省略できません。この値は、Stop hookによる続行の最中に true になります。Claude Codeは、Stop hookが連続8回続行させた後は次のブロックを無視してターンを終えます。8回の上限は CLAUDE_CODE_STOP_HOOK_BLOCK_CAP で変えられますが、docstringを埋める作業は数回で収束するので、既定のままで足ります。
stderrに流す内容を MISSED の行に絞っているのは、Claude Codeの文脈に入るのが「直す対象だけ」になるからです。表全体を渡すと、COVERED の行までトークンを使います。
落とし穴
hookのシェルにinterrogateが見つからない
仮想環境に interrogate を入れている場合、hookを実行するシェルのPATHに通っていないことがあります。上のスクリプトから存在確認を外すと、command not found(終了コード127)も非0として扱われ、MISSED が空のまま「カバレッジ不足」としてClaude Codeを止めてしまいます。そのため冒頭で command -v interrogate を確かめ、見つからなければ警告だけ出して exit 0 にしています。仮想環境のものを使うなら、.venv/bin/interrogate のように絶対パスで呼ぶか、プロジェクトの実行ラッパー経由にします。WindowsでPythonのhookを書く場合の文字化けは、Python hookの文字化けの記事で扱っています。
しきい値は現状から上げていく
既存プロジェクトにいきなり fail-under = 100 を入れると、無関係な作業のたびにStop hookが働き、Claude Codeが関係ない関数のdocstringを書き始めます。現在のカバレッジを基準にして、段階的に上げる運用が現実的です。
しきい値を段階的に上げる手順
- 1
現状を測る
fail-underを書く前にinterrogate -c pyproject.toml srcを走らせ、RESULT行のactualの値を控えます。 - 2
現状に近い値で固定する
fail-underを現状の少し下に置きます。新しく足したコードがdocstringなしで入ることだけを止められます。 - 3
ディレクトリ単位で埋めてから上げる
パッケージごとに埋めさせ、全体の数字が上がったら
fail-underを引き上げます。
対象を絞るには、interrogate にパスを渡します。interrogate -c pyproject.toml src/billing なら、billing 配下だけの判定になります。Claude Codeに「今回は src/billing だけ」と頼む場合も、Stop hookの対象パスを同じ範囲にそろえておくと、頼んでいない領域で止められません。
CIで同じ基準を使いたい場合は、pre-commitのhookが用意されています。repos に https://github.com/econchick/interrogate を登録し、args: [--quiet, --fail-under=95] と pass_filenames: false を指定する形です(pass_filenames: false は、pyproject.toml や setup.cfg で除外ファイルを設定しているときに必要になります)。Stop hookで手元の作業を締め、pre-commitやCIでマージ前に締める二段になります。
つまずきやすい点
googleスタイルの設定を誤解する:--style googleは、クラスのdocstringか__init__のdocstringのどちらかがあれば両方を済みとして数える指定です。docstringの書式(Args/Returnsの書き方)を決める設定ではありません。-i/--ignore-init-methodとは同時に使えません- オーバーロードやプロパティが未記載に出る:
@typing.overloadの関数は-O、プロパティのsetter/getter/deleterは-P(setterだけなら-S)で外せます。型スタブの.pyiを数えたいときは--ext pyiを足します - Stop hookが毎回走る: Stopは「タスクの完了時」ではなく、Claudeが応答を終えるたびに発火します。短い質問に答えただけの応答でも走るので、しきい値を現状より高く置くと、docstringと無関係な場面で止められます
- テストやドキュメントの生成物が混ざる:
exclude = ["tests", "docs", "build"]のように、数えたくない場所は設定で外します
まとめ
interrogateは「どこが足りないか」を数える道具で、Claude Codeは「足りない場所に書く」道具です。数字が合っても説明が正しいとは限らないので、docstring以外が変わっていないことをASTで確かめ、Args と Raises を本体の行と対応づけて読む二重の確認を組み合わせるのが、この構成の要点です。
Sphinxで文書をビルドしている場合、docstringの欠落はinterrogate、記法の崩れや参照切れはビルド警告と、見る場所が分かれます。後者はSphinxのビルド警告を直させる記事にまとめています。ruffやmypyと同じ検証ループにdocstringの確認を足すなら、ruffとmypyの併用の構成に interrogate を1行加える形が収まりやすいでしょう。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。