Claude Media
Claude Codeでpyright strictを段階導入する手順 — ディレクトリ単位で進める

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. 1

    strictリストに1ディレクトリを足す

    pyrightconfig.jsonのstrictにパスを1つ追加します。

  2. 2

    件数を数える

    pyright --outputjsonの結果をディレクトリ別に集計し、基準値を記録します。

  3. 3

    Claude Codeにそのディレクトリだけ直させる

    対象外のファイルには触れないよう、プロンプトと権限で範囲を絞ります。

  4. 4

    0件を確認して1コミット

    対象ディレクトリのエラーが0になってから、次のディレクトリへ進みます。

pyright strictのstrictリストは何を厳しくするのか

pyrightの設定ファイルには、strictというパスの配列があります。ここに書いたディレクトリやファイルは、ファイル先頭に# pyright: strictコメントを書いたのと同じ扱いになります。パスにはワイルドカードの**、*、?が使えます。

厳しくなる中身は、診断ルールの既定値表で確かめられます。standardではnoneだったルールが、strictではerrorに変わります。

設定standardstrict
reportMissingTypeStubsstandardnonestricterror
strictListInferencestandardfalsestricttrue
strictDictionaryInferencestandardfalsestricttrue
strictSetInferencestandardfalsestricttrue

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の範囲では設定でルールを切れないことだけ、先に頭に入れておくと詰まりません。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. microsoft.github.io/pyright
  2. code.claude.com/docs/en/memory
  3. code.claude.com/docs/en/permissions
  4. code.claude.com/docs/en/common-workflows
この記事を共有:XはてブLinkedIn