iimon TECH BLOG

iimonエンジニアが得られた経験や知識を共有して世の中をイイモンにしていくためのブログです

pre-commit × mypy を Docker イメージで動かして依存関係エラーを回避する方法

こんにちは! 株式会社iimonでエンジニアをしている遠藤です。

今携わっているプロジェクトの Python バックエンドで、コミット時に mypy を走らせて型チェックをするようにしたいと思いました。
ただ、以前別のプロジェクトで同じことを試したときに、依存関係が不足してエラーにハマった経験があります。

そこで今回は、そのときのエラー例と、最終的に Docker イメージを使って解決できた方法 をまとめてみました。

※本記事は --ignore-missing-importsを オフ にしている前提で書いています。 このオプションをオンにすると未解決の import は無視されますが、型チェックの網羅性が落ちる点には注意が必要です。

pre-commitについて

pre-commit は Git のフックを便利に管理できるツールです。

  • コードフォーマット(black, isort など)

  • 静的解析(flake8, ruff など)

  • 型チェック(mypy)

といった処理をコミット前に自動で実行できます。
レビュー前に最低限のコード品質を担保できるのがメリットです。

公式 hook を使ったときのエラー事例

.pre-commit-config.yaml は次のような設定でした。

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: v1.10.0
    hooks:
      - id: mypy
        args: [--config-file=mypy.ini]

pre-commit はこの設定で独自の仮想環境を作り、その中で mypy を実行します。
しかし、この環境にはプロジェクト依存が入っていないため、次のようなエラーが出ます。

error: Cannot find implementation or library stub for module named "django"
error: Cannot find implementation or library stub for module named "ninja"

これを回避するには additional_dependenciesに不足ライブラリを書き足す必要があります。

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: v1.10.0
    hooks:
      - id: mypy
        args: [--config-file=mypy.ini]
        additional_dependencies:
          - django-stubs
          - django-ninja

ただし、この方法には次のような課題があります。

  • 依存が増えるたびに追記が必要

  • requirements.txt / pyproject.toml と二重管理になる

  • mysqlclient など C拡張ライブラリはホスト環境にヘッダや mysql_config がないとインストール自体が失敗する

特に 3 つ目は開発者の環境によって動いたり動かなかったりして面倒でした。

macOS の例だと、

OSError: mysql_config not found

といったエラーが出るため、brew install mysqlなどでヘッダやライブラリを別途入れる必要がありました。

ここでは例として mysqlclient を挙げましたが、実際には他の C 拡張ライブラリや stub が原因になるケースもあります。 必ずしも必要になるわけでもないですが、どちらにしても「Docker の開発環境でそのまま mypy を走らせられれば楽そうだな」と考えるようになりました。

Docker イメージを使った解決方法

公式ドキュメントをちゃんと読んだら、なんだかそれっぽいことが書いてありました。

docker_image

dockerフックのより軽量なアプローチ。docker_image "言語" は、既存の docker イメージを使ってフック実行ファイルを提供します。

docker_image フックは、便利なことにローカルフックとして設定できます。

エントリは、使用する docker タグを指定します。イメージに ENTRYPOINT が定義されていれば、実行ファイルをフックするために特別なことは何も必要ありません。コンテナで ENTRYPOINT が指定されていない場合や、 エントリでエントリーポイントを変更したい場合は、 それを指定します。

https://pre-commit.com/#docker_image

要するに、docker_image 言語 という仕組みがあり、既存の Docker イメージを使ってフックを実行できるようです。

そこで「普段の開発で使っている Docker イメージの中で mypy を動かす」ようにしました。
つまり、pre-commit 独自の仮想環境を使わず、プロジェクト用にビルドしている Docker イメージをそのまま利用します。

書き直した.pre-commit-config.yamlは以下のようになりました。

# .pre-commit-config.yaml
repos:
    - repo: local
    hooks:
      - id: mypy-check
        args: [--config-file=mypy.ini]
        name: Run mypy inside project Docker image
        language: docker_image
        entry: --entrypoint mypy myproject-api:latest
        types: [python]
  • repo: local
    → ローカル定義のフック

  • language: docker_image
    → Docker イメージを使って実行

  • entry
    --entrypoint mypyでエントリポイントを上書きし、イメージ名 myproject-api:latest を指定している。イメージ名は自分の環境に合わせてください。

これで依存不足のエラーがなくなり、快適に mypy を走らせられました!

注意点

  • イメージ側に mypy と依存ライブラリがインストール済みであることが前提です(Dockerfile の pip install で入れておく)

  • イメージ名はチームで統一されていることが前提です

  • stub パッケージなど型用ライブラリは、いずれにしても requirements.txt / pyproject.toml に追加が必要です

まとめ

pre-commit は便利ですが、独自の仮想環境で動くため依存不足のエラーに悩まされることがあります。
特に C 拡張ライブラリでは環境依存のトラブルも起きやすいです。

docker_image を使えば、普段の開発で使っている Docker 環境をそのまま流用できるため、このような課題をシンプルに回避できます。 あくまで私のケースですが、同じように「mypy が pre-commit でうまく動かない…」と悩んでいる方の参考になれば嬉しいです。

現時点では実際に業務で運用したわけではないので、今後も知見が溜まったら追記していく予定です。

最後までお読みいただき、ありがとうございました! もっといい方法や記事の内容に誤りなどありましたら、ご指摘いただけますと幸いです。

また、弊社ではエンジニアを募集しております。 ぜひカジュアル面談でお話ししましょう! ご興味ありましたら、ご応募ください!  Wantedly / Green

参考資料

pre-commit.com

github.com

mypy.readthedocs.io

stackoverflow.com