Inside of LOVOT

GROOVE X 技術ブログ

2.8万件のエラーを潰して大規模 Python プロジェクトを mypy strict 化した話

この記事は、GROOVE Xアドベントカレンダー2025 の3日目の記事です。

はじめに

こんにちは!ふるまいチームの橋本です。
普段は LOVOT の意思決定エンジンである NeoDM の開発を行っています。
今回は、Pythonで書かれたこの大規模な意思決定エンジンに対し、型安全性を高めるために mypy の strict モード を導入した取り組みについて紹介します。

NeoDM vs 型エラー

なぜ今、型安全性に取り組むのか

きっかけは「ぬるぽ」

あるリリースにおいて、NeoDMの一部機能で実行時エラーが発生してしまいました。
原因はいわゆる「ぬるぽ」(Pythonでは None に対するアクセスによる AttributeError)です。
変数が None かどうかのチェックを漏らした状態で参照してしまったためでした。
このバグは特殊な条件下でのみ発生するためQAでの検出が難しく、本番環境で顕在化してしまいました。
しかし、静的解析(Type Checker)が機能していれば事前に気づけた可能性が高いものでした。

既存の mypy 運用の限界

NeoDM は Python で実装されていますが、Python は動的型付け言語であり、一般的に静的型付け言語と比較すると型安全性が低いとされています。
ただし Python も型ヒント (Type Hint) をサポートしており、mypy などの型チェッカーを利用することで型安全性を高めることができます。

NeoDMでは以前から mypy を導入していました。
しかし、当時の運用は型ヒントの記述が必須ではなく、チェックの厳格さも緩い設定でした。

特に問題だったのが Any 型の黙認 です。
mypy 変数の型が推定できない場合、それを Any として扱います。
「何でもあり」な Any 型になってしまうと、その変数を扱う以降の処理では型チェックが事実上無効化されます。
その結果、今回のような None チェック漏れも見逃されてしまっていました。

また、型ヒントが不十分なコードは、新規参入した開発者や、最近導入が進んでいる Coding Agent (AI) にとって可読性が低いという問題も抱えていました。

28,631件のエラー

これらの問題を解決するため、コードベース全体で mypy --strict(厳しいチェックモード)をパスさせることを目標にしました。

試しに、当時のコードベースに対して strict モードでチェックを実行してみたところ...

> mypy --strict .
Found 28631 errors in 808 files

28,631件。
普通なら「そっ閉じ」する数字だと思います。
しかし、同じような事故を再発させないためには CI で型検査すべきなのは明らかです。
「以後気をつけます」は当てになりません。

再発防止と可読性のため、この大量のエラーを解消するトライを始めました。
加えて、最近は Coding Agent の性能が上がっているので案外簡単に対応できるのでは?とやや楽観的に考えていた節もありました。

移行戦略:モジュール単位で「後戻りさせない」

800以上のファイルをまとめて修正するのは現実的ではありません。
修正中にも他の開発者がコードを変更し、差分が衝突し続けるからです。

そこで、モジュール単位で段階的に strict 化する という戦略を取りました。

  1. 特定のモジュール(ディレクトリ)の型ヒントを修正する。
  2. 修正が完了したモジュールを「strict チェック対象リスト」に加える。
  3. CI でそのリストに含まれるモジュールに対して strict チェックを強制する。

これにより、一度修正したモジュールは二度と型安全性が低下しない、いわば「後戻りさせない」仕組みを作りました。

部分的に strict チェックを行う工夫

ここで技術的な課題がありました。
mypy はインポート先のモジュールも追跡してチェックを行います。
そのため、まだ strict 化していないモジュールに依存しているファイルをチェックすると、修正対象外のファイルの型エラーまで報告されてしまい、CIをパスさせることができません。

これを回避するため、エラー出力を grep でフィルタリングし、「対象モジュールのエラーだけを検出する」スクリプトを作成しました。

# check_strict_modules.sh

# チェック対象のモジュール一覧をスペース区切りで指定  
STRICT_MODULES="module_a module_b"

# 対象モジュールを正規表現のOR条件(moduleA|moduleB|...)に変換  
PATTERN_BODY=$(echo "$STRICT_MODULES" | sed 's/ /|/g')  
REGEX="^($PATTERN_BODY)"

# mypyを実行し、エラー出力をキャプチャ  
# 終了コードは無視するために || true をつける  
ERRORS=$(mypy --show-traceback --show-error-codes --strict $STRICT_MODULES 2>&1 || true)

# 対象モジュールに関するエラーのみを抽出  
FILTERED_ERRORS=$(echo "$ERRORS" | grep -E "$REGEX" || true)

if [ -n "$FILTERED_ERRORS" ]; then  
    echo "$FILTERED_ERRORS"  
    echo "Found strict type errors in target modules."  
    exit 1  
else  
    echo "Success: No strict type errors found in target modules."  
fi

AI は銀の弾丸になったか?

2.8万件の修正をすべて手動で行うのは苦行です。
そこで、2025年らしく Coding Agent の活用を試みました。

プロンプトエンジニアリングの試行錯誤

mypy の実行と修正を自律的に繰り返してもらうため、プロンプトを継ぎ足していった結果、以下のようなプロンプト*1になりました。

mypy --strict . head -n 40 を実行し、エラーを修正して。  
変更は必要最低限に抑えて。  
原則として Any や 'TypeName' のような文字列での型付けは使わないで。  
ただし **kwargs は基本的に Any で良い。  
ignore は絶対に使わず、必要と思うなら許可を取って。  
型ヒントが必要ない箇所にはつけないで。  
@contextmanager, @asynccontextmanager がついているメソッドの返り値にはそれぞれ Iterator, AsyncIterator を使うと良い。  
np.ndarray は使わず、npt.NDArray[np.float64] などを使って。
(npt は import numpy.typing as npt)

(以下、NeoDM特有の型に関する説明が続く...)

結果と学び

結論から言うと、AI だけでの全自動修正は難しく、人手による修正が不可欠 でした。

  • 見直しの必要性: 深いドメイン知識がないと正しい型付けが難しい箇所もあり、AI が間違えることもある*2ため、結局すべて見直す必要がありました。それなら初めから人手 + Copilot などの自動補完で十分でした。
  • リファクタリングの判断: 適切に型をつけるにはコード構造の見直しが必要な場面もあり、その判断はケースバイケースになるため、AI に任せるのは難しかったです。
  • コーディング以外の対応: 場合によっては、対象のコードに詳しい開発者に仕様を確認する必要があり、コーディングだけでは済まない部分もありました。

初めのうちは Coding Agent に頼ってみたのですが、途中から人手で修正する割合が増えていきました。 ただ、AI 未だに猛スピードで進化しているので、半年後には100%任せられるようになっているかもしれません...

AI によるレビュー支援

修正作業と並んで大変だったのがレビューです。
ほとんどの変更は型ヒントの追加や簡単な修正であり、その正しさは mypy によってチェックできますが、リファクタした箇所はバグを埋め込んでしまうリスクがあります。
そのためレビューは必須ですが、内容を一つ一つ確認するのはなかなか骨が折れます。

初めのうちは人手でレビューしていたのですが、途中からレビューにも AI を導入しました。
具体的には Pull Request の差分を AI に渡し *3、問題がありそうな箇所を指摘してもらうようにしました。

工夫した点: 複数回レビューさせる
LLM は確率的に回答するため、同じ内容でも何度かレビューさせることで、1回目では見逃されたバグを検出できることがありました。

実際に AI の指摘によりバグを発見できたケースもあり、レビューの品質向上と効率化に大きく貢献しました。

成果とまとめ

数ヶ月にわたってスキマ時間にコツコツ修正を重ね、ついに NeoDM 全体で strict モードが通るようになりました。 気持ち良いですね!

> mypy --strict .
Success: no issues found in 974 source files

取り組みの成果:

  1. 安全性の向上: None チェック漏れなどの単純なバグは CI で確実に弾けるようになりました。
  2. 潜在バグの発見: 修正プロセスの中で、今まで顕在化していなかったバグをいくつか修正できました。
  3. コード品質の指標: 「型が書きにくいコードは設計が悪い」という指標になり、リファクタリングのきっかけが生まれやすくなりました。
  4. AIフレンドリーなコードへ: 型情報が充実したことで、今後の開発で Coding Agent がより正確にコードを理解・修正できるようになると期待しています。

型ヒントを厳密に書くことは一見手間にも思えますが、バグ調査の時間削減や、将来的な開発効率を考えれば、十分にお釣りがくる投資だと感じています。

さいごに

今回は、Python プロジェクト NeoDM の型安全性を高めるための取り組みについて紹介しました。

GROOVE Xでは、一緒に働く仲間を募集しています!
少しでも興味を持ってくださいましたら、下記のリンクをご覧ください。

recruit.jobcan.jp

*1:残念ながら、特に有用なプロンプトではないと思います。これくらい練ってみたがさほど上手くいかなかった、という記録です。

*2:既存のコードに誤った型が付いており、それに引きずられて AI が間違えるというケースもありました。

*3:細かい話ですが、レビューには主に Claude Code 等の Coding Agent ではなく、Gemini の Web App を使いました。 変更量が膨大な PR を従量課金の AI に見てもらうのが怖かったからです。