Claude Codeでpyright strictを段階導入する手順 — ディレクトリ単位で進める
pyrightのstrictを全体に掛けると大量のエラーが出ます。strictリストにディレクトリを1つずつ足し、--outputjsonで件数を数えながらClaude Codeに直させる手順と、スタブ不足の片付け方です。
pyrightのstrictは、リポジトリ全体に一度に掛けないのが現実的です。設定ファイルのstrictにディレクトリを1つずつ足し、足した範囲のエラーが0になってから次へ進めます。
既存のPythonコードにstrictを掛けると、型注釈の欠落とUnknownの連鎖で一気にエラーが出ます。これをClaude Codeにそのまま渡すと、広い範囲を一度に書き換えて差分が読めなくなります。範囲を区切り、件数で進み具合を測り、完了条件をCLAUDE.mdに固定する。この3点で、直す作業を小さな単位に分けられます。
strictを足してから次へ進むまでの1周
- 1
strictリストに1ディレクトリを足す
pyrightconfig.jsonのstrictにパスを1つ追加します。 - 2
件数を数える
pyright --outputjsonの結果をディレクトリ別に集計し、基準値を記録します。 - 3
Claude Codeにそのディレクトリだけ直させる
対象外のファイルには触れないよう、プロンプトと権限で範囲を絞ります。
- 4
0件を確認して1コミット
対象ディレクトリのエラーが0になってから、次のディレクトリへ進みます。
pyright strictのstrictリストは何を厳しくするのか
pyrightの設定ファイルには、strictというパスの配列があります。ここに書いたディレクトリやファイルは、ファイル先頭に# pyright: strictコメントを書いたのと同じ扱いになります。パスにはワイルドカードの**、*、?が使えます。
厳しくなる中身は、診断ルールの既定値表で確かめられます。standardではnoneだったルールが、strictではerrorに変わります。
| 設定 | standard | strict |
|---|---|---|
| reportMissingTypeStubs | standardnone | stricterror |
| strictListInference | standardfalse | stricttrue |
| strictDictionaryInference | standardfalse | stricttrue |
| strictSetInference | standardfalse | stricttrue |
typeCheckingModeをstrictにすると全体に効きますが、strictリストなら範囲を限れます。段階導入にはこちらが向きます。typeCheckingModeはstandardのまま、変換が終わったディレクトリだけをリストに足していく形です。
手元のpyright 1.1.390で、billingだけをstrictにした小さなプロジェクトを試しました。設定は次のとおりです。
{
"include": ["src"],
"strict": ["src/billing"]
}既存のPythonプロジェクトでpyproject.tomlに設定をまとめているなら、同じ内容を[tool.pyright]に書けます。
[tool.pyright]
include = ["src"]
strict = ["src/billing"]pyrightconfig.jsonとpyproject.tomlの両方があるときは、pyrightconfig.jsonが常に優先されます。移行中に両方を置くと、TOML側の変更が効かず戸惑うので、どちらか一方に寄せます。
billingには引数に型注釈のない関数と、使っていないimportを置き、legacyには同じく注釈のない関数を置きました。実行すると、strictに入れたbillingだけが指摘されます。
pyright出力はこの形でした(抜粋、パスは省略)。
src/billing/calc.py:1:8 - error: Import "requests" is not accessed (reportUnusedImport)
src/billing/calc.py:3:11 - error: Type of parameter "items" is unknown (reportUnknownParameterType)
src/billing/calc.py:3:11 - error: Type annotation is missing for parameter "items" (reportMissingParameterType)
src/billing/calc.py:4:31 - error: Type of "i" is unknown (reportUnknownVariableType)legacyのエラーは1件も出ません。リストに入れていないので、standardの基準で通っているためです。この「入れた場所だけ厳しくなる」性質が、段階導入の土台になります。
同じ実行でrequestsのimportにreportMissingModuleSourceの警告も1件出ました。これはstubは見つかったがソースが見つからないときの診断で、既定の重大度は全モードでwarningです。strictにしてもerrorにはならないので、エラー件数の集計からは外れます。
進み具合を--outputjsonで数える
件数の管理は、Claude Codeの自己申告ではなくpyrightの出力で行います。--outputjsonを付けると、診断がJSONで返ります。公式のコマンドライン説明によると、構造はgeneralDiagnosticsの配列と、errorCountなどを持つsummaryです。終了コードは、エラーが1件でもあれば1、なければ0です。
ディレクトリ別に数える小さなスクリプトをcount_errors.pyとして置いておくと、毎周の基準値が取れます。
import json
import subprocess
from collections import Counter
out = subprocess.run(["pyright", "--outputjson"],
capture_output=True, text=True).stdout
report = json.loads(out)
by_dir, by_rule = Counter(), Counter()
for d in report["generalDiagnostics"]:
if d["severity"] != "error":
continue
by_dir[d["file"].split("/src/")[-1].split("/")[0]] += 1
by_rule[d.get("rule", "-")] += 1
print("total:", report["summary"]["errorCount"])
for name, n in by_dir.most_common():
print(f"{n:4d} {name}")
for name, n in by_rule.most_common(5): # 上位5件のみ表示
print(f"{n:4d} {name}")legacyもリストに足してcount_errors.pyを実行すると、legacyの分が加わった次の出力になりました。ルール別は上位5件だけを表示しています。
total: 10
6 billing
4 legacy
3 reportUnknownParameterType
2 reportMissingParameterType
2 reportUnknownVariableType
1 reportUnknownMemberType
1 reportUnusedImportディレクトリ別の件数は、次に足す候補を選ぶ材料になります。件数が少なく、他のディレクトリから呼ばれることの多い土台のモジュールから足すと、Unknownが呼び出し側へ広がる量が減ります。ルール別の上位は、Claude Codeへの指示を絞る材料です。reportUnknownParameterTypeが多ければ、まず引数の注釈から直させます。
reportMissingTypeStubsを片付ける
strictに入れたとたん、サードパーティのimportにreportMissingTypeStubsのエラーが出ることがあります。型スタブもインライン型情報もないパッケージが対象です。検証用に、型情報を持たないパッケージbottleを仮想環境に入れて、strictの範囲でimportしてみました。
src/billing/banner.py:1:8 - error: Stub file not found for "bottle" (reportMissingTypeStubs)
src/billing/banner.py:4:5 - error: Type of "run" is partially unknown (reportUnknownMemberType)スタブ不足のエラーと、その先で型がUnknownになるエラーが組で出ています。片付け方は2通りあります。
- 設定でルールを切る: strictの範囲では効きません。公式の説明どおり、strictモードの上書きは厳しくする方向にしか効かないためです。実際に
executionEnvironmentsでreportMissingTypeStubsをfalseにしても、エラーは残りました。 - スタブを生成する:
pyright --createstub bottleで、プロジェクト直下のtypingsにスタブが作られます。stubPathの既定が./typingsなので、設定を足さずに拾われます。
pyright --createstub bottle
pyright src/billing/banner.pyスタブを作ったあとの再実行では、スタブ不足のエラーが消えました。残ったのは、生成スタブの引数が...になったことによるreportUnknownMemberTypeの1件です。生成されるスタブは骨組みなので、戻り値や引数の型は手で補う必要があります。ここはClaude Codeに任せやすい作業です。呼び出している箇所の使い方を読ませ、使う関数だけ注釈を書かせると、スタブ全体を整えるより小さく済みます。
スタブを作るか、そのimportを含むファイルをリストの外に置くか。使う関数が少ないパッケージならスタブを作り、広く使われるパッケージなら公式の型付きの代替がないかをまず調べる、という順で考えると迷いません。
落とし穴
strictに足した直後にimportが赤くなる
足したディレクトリが依存しているのは、そのディレクトリ自身のコードだけではありません。サードパーティのimportがスタブ不足なら、コード側が正しくてもreportMissingTypeStubsで件数が増えます。リストに足す前に、そのディレクトリが何をimportしているかを一覧にしておくと、増える件数を予想できます。
Claude Codeに直させる範囲を固める
Claude Codeは、CLAUDE.mdを強制される設定ではなく、読んで従うコンテキストとして扱います。memoryのドキュメントも、検証できるほど具体的に書くよう勧めています。「型を厳密にして」ではなく、実行するコマンドと完了条件を書きます。
## 型チェック(pyright)
- 完了条件: `pyright` の終了コードが 0 であること
- strict の範囲は pyrightconfig.json の `strict` が正。今回のタスクで指定されたディレクトリ以外は編集しない
- `# type: ignore` と `# pyright: ignore` は足さない。必要なら理由を報告して止まる
- スタブが無いパッケージは `pyright --createstub <名前>` で作り、使う関数だけ注釈を補うCLAUDE.mdは200行以内を目標にするよう案内されています。ここに書くのは、pyrightに関する数行で足ります。
実行の許可はpermissionsで与えます。ルールのBash(ls *)のように、末尾に空白付きの*を置くと、引数なしの素のコマンドにもマッチします。
{
"permissions": {
"allow": ["Bash(pyright *)"]
}
}これでpyrightとpyright src/billingの両方が確認なしで走ります。編集の範囲は権限ではなくプロンプトで指定するほうが手軽です。毎周のプロンプトは、たとえば次のような形にします(例示です)。
pyrightconfig.json の strict に src/billing を追加済みです。
`pyright` を実行し、src/billing のエラーだけを直してください。
src/billing 以外のファイルは変更しないでください。
直し終えたら再実行し、終了コードが 0 であることを報告してください。「直す前に件数を報告させる」一文を足すと、基準値と結果の差を見比べやすくなります。ただしClaudeの報告は目安で、判定は自分でcount_errors.pyを回して行います。
直し方の順番と、足してはいけないもの
エラーを一気に消したいとき、手っ取り早いのは# type: ignoreの貼り付けです。件数は0になりますが、型の穴は残ります。CLAUDE.mdで禁止しておき、本当に必要な箇所だけ人が許可する運用が安全です。
もう一つ、reportUnnecessaryTypeIgnoreCommentというルールがあります。不要になった# type: ignoreと# pyright: ignoreを検出するもので、既定はnoneです。既存コードに無効なignoreが溜まっているなら、これをerrorにして一掃してから進めると、strictの件数が実態に近づきます。
一周ごとの粒度は、1ディレクトリを1コミットにします。ファイル数が多いなら、strictにはディレクトリではなく個別のファイルも書けるので、ファイル単位に落とせます。同じ段階移行の考え方は、Swift 6のstrict concurrencyを段階的に直す手順でも扱っています。言語は違いますが、「範囲を区切る、件数で測る、0件で閉じる」という骨格は共通です。
ruffやmypyと併用するとき
pyrightを入れる場合も、リンターと組み合わせる形は変わりません。実行順と完了条件をCLAUDE.mdに並べる方法は、ruffとmypyを併用する検証ループで扱っています。mypyの代わりにpyrightを使うなら、完了条件のコマンドを差し替えるだけで済みます。
2つの型チェッカーを同時に走らせる構成は、診断が食い違ったとき、どちらの基準で完了とするかが曖昧になります。型チェッカーは1つに決め、完了条件のコマンドもそれ1本にしておきます。
範囲を広げる順番の決め方
最後に、どの順でリストに足すかです。次の3つの基準を組み合わせると決めやすくなります。
足す順番を決める3つの基準
依存される側から
共通ユーティリティやモデル定義のように、他から呼ばれるディレクトリを先に厳しくします。型が確定すると、呼び出し側の
Unknownが減ります。件数の少ない順
count_errors.pyの結果で件数が少ないところから足すと、1周が短く終わり、コミットも小さく保てます。変更頻度の高い場所
日々手が入るディレクトリは、新しいコードが注釈なしで入るのを早く止められます。
どの基準でも、足す前の件数と足した後の件数を記録しておきます。リストの最後に、プロジェクト全体を指すパスが入ったら、typeCheckingModeをstrictにしてstrictリストを畳めます。
まとめ
strictの段階導入で効くのは、Claude Codeの賢さより区切り方です。リストに1つ足し、pyrightの出力で数え、0件で閉じる。CLAUDE.mdには完了条件とignore禁止を書き、スタブ不足は--createstubで作ります。strictの範囲では設定でルールを切れないことだけ、先に頭に入れておくと詰まりません。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。