Claude Codeでvultureを使う — Pythonの未使用コードを削る前に動的参照を確かめる
vultureでPythonの未使用コードを洗い、getattrやデコレータ経由の誤検知をwhitelistで外してからClaude Codeに削らせる手順。信頼度の読み方とStop hookでの検証まで。
vultureは、Pythonの未使用コードをastで解析して洗い出すツールです。Claude Codeに「使っていないコードを消して」と頼む前に、vultureのレポートを渡して候補を絞ると、削除の範囲を小さく保てます。ただしPythonは動的な言語で、getattrやデコレータ経由で呼ばれるコードは「未使用」と報告されます。レポートをそのまま削除指示にすると、動いているコードまで消えます。
ここでは、手元のvulture 2.16(Python 3.12)で小さなプロジェクトを動かした出力をもとに、信頼度の読み方、誤検知の振り分け、whitelistの作り方、Claude Codeに削らせる流れを順に書きます。JS/TS側の同じ作業はClaude Codeでknipを使う手順にあります。
vultureの信頼度は種類で決まる
vultureは見つけたコードの塊ごとに、60%から100%の信頼度を付けます。値は「そのコードが未使用である確率」の精密な見積もりではなく、コードの種類ごとに決まった目安です。READMEの区分は次のとおりです。
| コードの種類 | 信頼度 |
|---|---|
| 関数・メソッド・クラスの引数、到達不能コード | 信頼度100% |
| import | 信頼度90% |
| 属性、クラス、関数、メソッド、プロパティ、変数 | 信頼度60% |
READMEは、60〜90%の値を「非常に大まかな見積もり」と書いています。--min-confidence 100を付けると、解析した範囲で確実に使われないコードだけが残ります。
この表から読めるのは、関数やメソッドの未使用はすべて60%という点です。60%の群には本当に死んでいるコードと、動的に呼ばれているコードが混ざっています。人間かClaude Codeが1件ずつ仕分ける対象は、ほぼこの群です。
小さな例で実際の出力を見る
次のようなapp.pyを用意しました。Exporterの3つのメソッドのうち、実際に呼ばれるのはgetattr経由のexport_csvだけです。handle_pingはデコレータで辞書に登録され、legacy_reportは誰も呼びません。
検証に使った app.py
import os
import json
HANDLERS = {}
def register(name):
def deco(fn):
HANDLERS[name] = fn
return fn
return deco
class Exporter:
def export_csv(self, rows):
return ",".join(rows)
def export_json(self, rows):
return json.dumps(rows)
def export_xml(self, rows):
return "<rows/>"
@register("ping")
def handle_ping(request):
return "pong"
def legacy_report(data):
unused_total = sum(data)
return "done"
def run(fmt):
exporter = Exporter()
fn = getattr(exporter, "export_" + fmt)
return fn(["a", "b"])
if __name__ == "__main__":
print(run("csv"))vulture app.pyapp.py:1: unused import 'os' (90% confidence)
app.py:15: unused method 'export_csv' (60% confidence)
app.py:18: unused method 'export_json' (60% confidence)
app.py:21: unused method 'export_xml' (60% confidence)
app.py:25: unused function 'handle_ping' (60% confidence)
app.py:26: unused variable 'request' (100% confidence)
app.py:30: unused function 'legacy_report' (60% confidence)
app.py:31: unused variable 'unused_total' (60% confidence)終了コードは3(デッドコードあり)でした。8件のうち、本当に消してよいのはos、legacy_report、unused_totalの3件です。残りの5件は次のとおり、削ると壊れるか、削る前に判断が要ります。
export_csvはgetattrで呼ばれていて、実際に使われているexport_jsonはrun("json")で呼べるメソッドで、今のスクリプトには呼び出しがないだけexport_xmlも同じ理由で、run("xml")ならgetattr経由で到達できるhandle_pingはデコレータで辞書に登録されていて、辞書経由で呼ばれる想定requestは100%と出ているが、呼び出し側が引数を渡す関数の署名の一部
同名の関数や属性が別の場所で使われていると、vultureは未使用と判定しません。READMEによれば、解析はスコープを無視して名前だけを見るためです。つまり誤検知だけでなく、見逃しもあります。
誤検知を3つに振り分ける
Claude Codeに削除を頼む前に、レポートの各行を次の3つに分けます。振り分けの基準は「文字列や登録経由で参照されていないか」です。
getattrや文字列で組み立てた名前
getattr(exporter, "export_" + fmt)のように名前を文字列で組み立てると、ast上はexport_csvの参照が現れません。READMEの例も同じ型で、func_name = "greet"とgetattrの組み合わせでgreetが60%の未使用と出ます。READMEの推奨は、使われているコードをwhitelistに足すことです。
Claude Codeには、60%の各項目について「その名前を文字列として組み立てている箇所はないか」を調べさせます。例えば"export_" +のような接頭辞の連結、getattr、globals()、設定ファイルやYAML内の関数名の記述が手がかりです。
デコレータやフレームワークが呼ぶ関数
Flaskの@app.routeのように、デコレータが登録して外から呼ばれる関数は、コード上の呼び出しがありません。--ignore-decoratorsはこの型のためのオプションです。先の例に--ignore-decorators "@register"を付けると、handle_pingの行は消えました。
vulture app.py --ignore-decorators "@register"注意点があります。READMEのとおり、このオプションが無視するのはデコレータが付いた関数の名前だけで、引数と関数本体は対象外です。実際、同じ実行でrequestの100%は残りました。デコレータの引数は、@foo.bar(x, y)が@foo.barに、@foo.bar(x, y).bazが@に単純化されて扱われます。パターンが効かないときは、この単純化後の形に合わせます。
READMEは、--ignore-namesや--ignore-decoratorsよりwhitelistを推奨しています。whitelistは構文の正しさが自動で検査され、Pythonインタープリタに通せば、whitelistした名前が今もプロジェクトに存在するかまで確かめられるためです。
100%でも消せない引数
信頼度100%のrequestは、関数の本体で使っていないだけです。ディスパッチ側がhandler(request)と呼ぶなら、引数を削ると呼び出しが壊れます。READMEが示す回避策は、引数名を_requestのように先頭アンダースコアにするか、本体でdel requestと書くことです。vultureは先頭が_の変数を無視します。
100%の群にも、削除ではなく名前の変更が正解の行があります。「100%だから安全に消せる」と読むと、この罠に当たります。
whitelistの作り方と、作った直後の罠
whitelistは、使われているコードを「使った」と見せるためのPythonファイルです。READMEの例は、Greeter.greetと書いた1ファイルを解析対象に加える形です。
自動生成には--make-whitelistを使います。
vulture app.py --make-whitelist > whitelist.py
vulture app.py whitelist.pyos # unused import (app.py:1)
_.export_csv # unused method (app.py:15)
_.export_json # unused method (app.py:18)
_.export_xml # unused method (app.py:21)
handle_ping # unused function (app.py:25)
request # unused variable (app.py:26)
legacy_report # unused function (app.py:30)
unused_total # unused variable (app.py:31)落とし穴
生成したwhitelistをそのまま使うと、死んだコードも全部通る
--make-whitelistは、報告された全項目を「使用済み」として書き出します。この出力を渡した2回目の実行は、終了コード0(デッドコードなし)になりました。本当に消すべきlegacy_reportもunused_totalも含まれるためです。生成後に、実際は動的参照で使われている行だけを残し、残りは削除対象として行ごと外します。
READMEも、生成されるwhitelistは構文こそ正しいものの、Pythonで実行するには手直しが要ると書いています。上の例なら、残すのは_.export_csv、_.export_json、_.export_xml、handle_pingの4行です。export_jsonとexport_xmlは、その形式を今後も受け付けるなら残し、廃止するならrunから呼べる経路ごと消します。_.export_csvの_.は、vultureがメソッド名を書き出すときの表記です。
この仕分けこそ、Claude Codeに任せる意味がある作業です。生成物を渡して「各行について、動的参照で使われている根拠をコードから示せるものだけ残して」と頼むと、根拠のない行が削除対象として分かれます。根拠は、grepの結果の行番号とファイル名で出させます。
pyproject.tomlに設定を置く
オプションを毎回打たずに済むよう、READMEはpyproject.tomlの[tool.vulture]に設定を書く方法を示しています。ダッシュを外してアンダースコアに置き換えたキー名です。
[tool.vulture]
paths = ["src", "whitelist.py"]
min_confidence = 60
sort_by_size = true
ignore_decorators = ["@app.route", "@register"]
exclude = ["*/migrations/*", "*/.venv/*"]vultureはカレントディレクトリのpyproject.tomlを自動で探します。別の場所のファイルは--configで指します。コマンドラインの指定は、ファイル内の設定より優先されます。sort_by_sizeは未使用コードを行数順に並べるので、大きい塊から手を付けたいときに向きます。
これでClaude Codeにはvultureと打たせるだけで、毎回同じ条件の結果が返ります。
Claude Codeに削らせる手順
Claude Codeのbest practicesは、合否が返るチェックを持たせると、Claude自身が変更、実行、修正を繰り返せると説明しています。チェックの例にはテストやlinterが挙がっていて、CLAUDE.mdに書くべき最も有用な1行は、それを走らせるコマンドだとされています。vultureの掃除でも、同じ形に落とせます。
まずCLAUDE.mdに、検査コマンドと仕分けのルールを書きます。以下は、公式の例に沿って組んだ一例です。
# Commands
- 未使用コード検査: `vulture` (設定は pyproject.toml の [tool.vulture])
- テスト: `pytest -q`
# 未使用コードの削除ルール
- vulture の報告をそのまま削除しない。60% の項目は、名前を文字列で
組み立てる箇所(getattr / globals / 設定ファイル内の名前)を
検索してから判断する
- 動的に使われていた項目は whitelist.py に追記する。削除はしない
- 100% の「引数」は削除しない。署名を変えるときは呼び出し側も直す
- 1 回の削除は種類ごとにコミットを分け、削除のたびに pytest を通す実際の作業は、次の順に進みます。
vultureの結果をClaude Codeで仕分けて削る
- 1
レポートをファイルに残す
vulture --sort-by-size > vulture-report.txtでレポートを取ります。大きい塊から順に見られます。 - 2
動的参照を先に調べさせる
plan modeなどで編集を止めた状態で、60%の項目それぞれに、文字列参照やデコレータ経由の使用がないか検索させます。結果は「使われている」「根拠なし」の2列に分けて出させます。
- 3
使われている項目をwhitelistへ
動的参照が見つかった行だけを
whitelist.pyに書き、vultureをもう一度走らせて、その項目が消えたことを確かめます。 - 4
根拠のない項目を削る
削除は種類ごとに分けます。import、変数、関数の順だと、削った結果の影響が追いやすくなります。削るたびに
pytestを走らせます。 - 5
vultureを再実行する
READMEは、デッドコードを消したあとに再度実行するよう勧めています。削除で新たに未使用になるコードが出るためです。
テストの実行を手順に含めるのは、vultureが静的解析で、動的に呼ばれるコードを見逃せないからです。whitelistに入れそこねたコードを消しても、テストが通れば壊れなかったと言えますが、テストのない経路はこの確認の外です。
Stop hookで「未使用コードの検査」を終了条件にする
best practicesには、Stopフックで型検査を強制する設定例があります。コマンドがexit code 2で終わるとClaudeは停止できず、標準エラーに出した内容が理由として返ります。これをvultureに置き換えると、次の形になります。
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "vulture --min-confidence 100 1>&2 || exit 2"
}
]
}
]
}
}vultureはデッドコードがあるとexit code 3を返すので、|| exit 2でブロックの合図に変換しています。ここで--min-confidence 100にしているのは、60%の項目を終了条件にすると、whitelistの調整が終わるまで毎回ブロックされ、上限に達するまで同じ指摘が繰り返されるためです。ただし、さきほど見たとおり100%の群は引数と到達不能コードだけなので、この条件は「掃除の完了」ではなく「明らかな死んだ引数が残っていないこと」の確認になります。
公式の説明では、8回連続でブロックされると、Claude Codeは次のブロックを無効にしてターンを終えます。この上限はCLAUDE_CODE_STOP_HOOK_BLOCK_CAPで変えられ、詳細はStop hookの上限を扱った記事にあります。hooksでテストやlintを自動で回す設計は、hooksでテストを自動実行する記事に整理してあります。
運用で効いてくる点
vultureの特性を踏まえると、運用では次の点が分かれ目になります。
- 解析対象にテストも加えると、テストでしか使われていないコードや、テストのないコードを探せる(READMEが挙げる用途)
- 解析は名前だけを見るので、同名のメソッドが多いプロジェクトでは、見逃しが出る前提で読む
- 「ライブラリが外から呼ぶ公開API」は、このリポジトリの中に呼び出しがないので、60%の群に出やすい。公開APIのモジュールは
excludeかwhitelistで外す pre-commitにも組み込める。READMEは.pre-commit-config.yamlにrev: 'v2.3'以降のvultureのフックを書く例を示している
最後の点は、削除をClaude Codeに任せたあとに、人間のコミットで再発を止める役割です。
まとめ
vultureが60%で出す関数やメソッドは、死んだコードと動的に使われているコードの混合です。仕分けの前に消すと、getattrやデコレータ経由で動くコードが消えます。レポートを渡す前にwhitelistで誤検知を外し、--make-whitelistの出力はそのまま使わず根拠のある行だけ残し、100%の引数は削除より名前の変更を先に疑う。この順で進めれば、Claude Codeに任せる範囲は「根拠のある削除」に絞れます。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。