目次
30 章

型チェッカ設定と CI 統合

mypy / pyright / ruff の設定と衝突回避、pre-commit でローカル段階でブロック、GitHub Actions で PR 段階でブロックするまで。

5部(運用・パッケージング・テスト)の始まりです。4部までが「機能を作る方法」だったとすれば、5部は「そのコードを運用可能に仕上げる方法」です。本章はその第一歩 — 型検査を自動化 することです。

本書全体が型ヒント優先で書かれています。第9章 typing・Generic・Protocol第20章 typing 上級 で見た表現力は、ツールが強制しなければ時間とともに崩れていきます。本章はその規律をツールで維持する方法を扱います。

なぜ静的検証が必要なのか #

テストだけでは捕まえられないものがあります。

  • 呼び出されないコードパスの型エラー — テストが到達しなかった分岐。
  • None の見落としOptional[X] を受け取って .foo() を呼び出すミス。
  • API 変更後の呼び出し側の残骸 — ライブラリのバージョンを上げたときに消えた引数が残っているケース。
  • 誤った dict キー / 誤った enum 値 — 実行時にしか露呈しない typo。

静的解析はコードを実行せずに、こうした問題を すべての分岐について 捕まえます。単体テストの補完であり、代替ではありません。

ツール分担 — 何が何をするのか #

ツール役割備考
rufflinting + formatting + import 整列flake8 + black + isort 統合。Rust 製で非常に高速
mypy型検査(リファレンス実装)最も歴史が長く、互換性が広い
pyright型検査(Microsoft)mypy より速く、推論が強力。VS Code の Pylance が同じエンジン
pre-commitローカルのコミット段階で自動実行上記ツールを束ねるフレームワーク
GitHub ActionsPR 段階で同じ検証を再実行ローカルを飛ばした変更をブロックする安全網

核心原則は ローカルと CI で同じツール・同じバージョン・同じ設定 を使うことです。ローカルで通ったものが CI で失敗すれば、信頼が崩れます。

mypy と pyright — どちらを選ぶべきか #

どちらも PEP 484 の型ヒントを検査する静的解析器ですが、性格が違います。

mypy

  • Python のリファレンス実装。新しい PEP が最初に反映されます。
  • 推論が弱めです — 変数を一度型で絞っても、次の分岐で再び広がることがあります。
  • ライブラリの互換性が広いです — 古い stub もよく読みます。

pyright

  • Microsoft が作ったツール。VS Code の Pylance が同じエンジンを使います。
  • 推論が非常に強力です — 絞り込んだ型をよく保ちます。
  • strict モード(strict)が非常に厳しく、最初に有効にするとエラーが大量に出ます。
  • mypy より高速です。

実戦推奨: どちらか一つを選ぶ必要があるなら pyright です。エディタと CLI が同じエンジンを使うので一致性が高くなります。ただし外部ライブラリの stub が不足する場合は mypy のほうが寛容なこともあります。

本書は pyright を基準に説明します。

ruff — linting・formatting・import 整列を一つのツールで #

ruff が登場する前は flake8 + black + isort + pyupgrade を別々にインストールし、それぞれ設定ファイルを置き、pre-commit に 4 つの hook を登録していました。今はこの役割が ruff ひとつに統合され、Rust で書かれているのでかなり高速になりました。

インストール #

uv add --dev ruff

pyproject.toml 設定 #

pyproject.toml
[tool.ruff]
line-length = 100
target-version = "py314"
src = ["src", "app", "tests"]

[tool.ruff.lint]
select = [
  "E",    # pycodestyle errors
  "W",    # pycodestyle warnings
  "F",    # pyflakes
  "I",    # isort
  "B",    # flake8-bugbear (よくあるバグパターン)
  "C4",   # flake8-comprehensions
  "UP",   # pyupgrade (Python バージョンに合わせた構文アップグレード提案)
  "RUF",  # ruff 独自ルール
]
ignore = [
  "E501",  # line too long — formatter が処理
]

[tool.ruff.lint.per-file-ignores]
"tests/*" = ["S101"]  # テストでは assert を許可

[tool.ruff.format]
quote-style = "double"
indent-style = "space"

select が有効にするルールカテゴリです。最初は上のあたりから始めて、慣れてきたら N(命名)、D(docstring)、ANN(アノテーション強制)などを追加していきます。

実行 #

検査
uv run ruff check .
自動修正
uv run ruff check . --fix
フォーマット
uv run ruff format .

検査とフォーマットが分かれている点が重要です。ruff check は「このコードは間違っている」を知らせ、ruff format は「このコードの見た目を統一する」を行います。

pyright — 型検査の設定 #

インストール #

uv add --dev pyright

pyproject.toml 設定 #

pyproject.toml
[tool.pyright]
include = ["src", "app", "tests"]
exclude = ["**/__pycache__", "**/node_modules", "build", "dist"]
pythonVersion = "3.14"
typeCheckingMode = "standard"  # off / basic / standard / strict

# 段階的採用 — モジュールごとに違う設定を有効化
executionEnvironments = [
  { root = "app/core", reportMissingTypeStubs = "error" },
  { root = "app/services", reportMissingTypeStubs = "error" },
  { root = "tests", reportMissingTypeStubs = "none", reportPrivateUsage = "none" },
]

# 個別ルール調整
reportUnusedImport = "warning"
reportUnusedVariable = "warning"
reportMissingTypeStubs = "warning"
reportImplicitOverride = "error"

typeCheckingMode が核心です。

  • off — 有効になっていても事実上検査しない
  • basic — 明らかなエラーのみ
  • standard — 合理的なデフォルト(推奨される開始点)
  • strict — すべての検査を有効化。一気に有効にすると大きなコードベースでは数百のエラー

実行 #

uv run pyright

特定のファイルだけ検査したい場合は uv run pyright app/services/

pre-commit — ローカルのコミット段階でブロック #

pre-commit は Git hook フレームワークです。git commit の直前に登録した検査を実行し、失敗すればコミットをブロックします。

インストール #

uv add --dev pre-commit

設定ファイル #

.pre-commit-config.yaml
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v5.0.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
      - id: check-toml
      - id: check-added-large-files
        args: [--maxkb=500]
      - id: check-merge-conflict

  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.8.0
    hooks:
      - id: ruff
        args: [--fix]
      - id: ruff-format

  - repo: https://github.com/RobertCraigie/pyright-python
    rev: v1.1.390
    hooks:
      - id: pyright
        additional_dependencies:
          - fastapi
          - pydantic
          - sqlalchemy

pyright は外部ライブラリの型 stub を見るために、そのライブラリが同じ環境にインストールされている必要があります。pre-commit は隔離された venv で hook を回すので additional_dependencies で明示します。

インストールと初回実行 #

git hook インストール
uv run pre-commit install
全ファイルに一度回してみる
uv run pre-commit run --all-files

これ以降、git commit するたびに変更されたファイルに対して hook が自動で実行されます。失敗すればコミットがブロックされます。

hook を一時的にスキップする #

緊急 hotfix のような例外的な状況では git commit --no-verify でスキップできます。ただし PR 段階で同じ検証が再びブロックするので、結局は修正する必要があります。

GitHub Actions — PR 段階の安全網 #

ローカル hook をスキップした変更、または別の環境で作られた変更を捕まえる第二の網です。

.github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  lint-and-typecheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install uv
        uses: astral-sh/setup-uv@v4
        with:
          enable-cache: true

      - name: Set up Python
        run: uv python install 3.14

      - name: Install dependencies
        run: uv sync --frozen

      - name: Ruff check
        run: uv run ruff check .

      - name: Ruff format check
        run: uv run ruff format --check .

      - name: Pyright
        run: uv run pyright

  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_USER: test
          POSTGRES_PASSWORD: test
          POSTGRES_DB: test
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
      - uses: actions/checkout@v4

      - uses: astral-sh/setup-uv@v4
        with:
          enable-cache: true

      - run: uv python install 3.14
      - run: uv sync --frozen

      - name: Run tests
        env:
          DATABASE_URL: postgresql+asyncpg://test:test@localhost:5432/test
        run: uv run pytest -v --cov=app --cov-report=xml

      - uses: codecov/codecov-action@v5
        with:
          files: ./coverage.xml

いくつかのパターン。

  • concurrency — 同じ PR に push が続くと前の実行を自動キャンセル。CI 時間の節約。
  • uv sync --frozenuv.lock と正確に一致する環境。lock が合わないと失敗。
  • 2 つの job 分離 — lint/typecheck は速く終わるので test と並列で回します。素早いフィードバック。

段階的な型付け採用戦略 #

既存コードベースに strict を一気に有効にすると数百のエラーが流れ込み、絶対に消化できません。段階に分けて進めるべきです。

1段階 — basic から開始

[tool.pyright]
typeCheckingMode = "basic"

明らかなエラー(None の見落とし、attribute の typo)だけを捕まえます。すでに型ヒントが付いていればほぼ通過します。

2段階 — standard に昇格

typeCheckingMode = "standard"

追加の検査(return 型の欠落、Optional 処理など)が有効になります。

3段階 — モジュール単位の strict

[[tool.pyright.executionEnvironments]]
root = "app/core"
typeCheckingMode = "strict"

[[tool.pyright.executionEnvironments]]
root = "app/services"
typeCheckingMode = "strict"

新しく書いたモジュール、重要なモジュールから順に strict に上げていきます。

4段階 — 全体 strict、例外リストで管理

typeCheckingMode = "strict"

[[tool.pyright.executionEnvironments]]
root = "app/legacy"
typeCheckingMode = "standard"  # legacy だけ緩める

基本は strict、緩めた箇所を明示的に例外登録。「緩めた領域の大きさ」がそのまま技術的負債の大きさになります。

5段階 — strict 100%

legacy が空になるまで継続的にモジュールを移していきます。

よくある落とし穴 #

外部ライブラリに type stub がない #

error: Skipping analyzing "some_lib": module is installed, but missing library stubs or py.typed marker

3 つの選択肢があります。

  1. types-some_lib が PyPI にあるか確認(uv add --dev types-some_lib)
  2. 自分で stub を書く(stubs/some_lib.pyi)
  3. その import だけ無視(# pyright: ignore[reportMissingTypeStubs])

pyright と mypy の結果が違う #

2 つのツールの推論アルゴリズムが違います。両方を厳密な基準として同時に運用するのではなく、片方を基準ツールに決め、もう片方は IDE 補助程度に使います。本書では pyright を基準にします。

Any の伝染 #

ある関数が Any を返すと、その結果を受け取るすべての箇所が Any になります。reportUnknownReturnTypereportAny のような strict ルールがこれを捕まえますが、出発点は「外部ライブラリの境界でだけ Any、自分のコード内部では Any 禁止」です。

ruff と formatter の衝突 #

ruff が lint と format を両方担当すれば衝突はありません。ただし外部の black を併用すると規則がずれることがあります。本書では ruff format ひとつに統一します。

pre-commit hook のバージョンが古い #

.pre-commit-config.yamlrev: が固定されるので、定期的に上げる必要があります。

uv run pre-commit autoupdate

このコマンドがすべての repo の rev: を最新に更新します。四半期に一度くらいで十分です。

第29章の総合実習に適用する #

第29章 総合実習 — TODO API を完成させる で作ったプロジェクトに本章の設定をそのまま乗せると、完成度が一段上がります。

cd todo-api/
uv add --dev ruff pyright pre-commit
# 上の pyproject.toml / .pre-commit-config.yaml / .github/workflows/ci.yml を追加
uv run pre-commit install
uv run pre-commit run --all-files  # 初回通過
git add . && git commit -m "chore: setup typecheck and CI"

これで main ブランチには型検査・lint・フォーマット・テストを通過したコードしか入らなくなります。

練習問題 #

  1. ruff の select 拡張 — 本章のデフォルトの selectN(命名)、D(docstring)を追加してみてください。最初はどんなエラーが一番多く出ますか? per-file-ignores でテストフォルダだけ一部ルールを緩めるにはどう書きますか?
  2. pyright executionEnvironments — 自身のプロジェクト(なければ第29章の TODO API)で 1 つのディレクトリだけ typeCheckingMode = "strict" に上げてみてください。どんなエラーが一番多く出ますか? Optional の見落とし、Any の伝染、return 型の欠落のうち、どれが一番多いですか?
  3. CI キャッシュ最適化 — 上の GitHub Actions workflow の lint-and-typecheck job と test job が毎回依存関係を新しくインストールしています。astral-sh/setup-uv@v4enable-cache: true がどう動作するか確認し、uv.lock が変わっていない PR の 2 回目の run が 1 回目に対してどれだけ速くなるか比較してみてください。
注記
一行まとめ — 静的検証は ruff(lint・format)+ pyright(型)の 2 つのツールで十分です。pre-commit でローカル、GitHub Actions で PR 段階と、同じ検証を二重にブロックして初めて、コードベースの型安全性が時間が経っても保たれます。

次の章は logging と観測性 です。静的検証で「コードの正確性」をブロックしたら、次の段階は「運用中に何が起きているかを見えるようにする方法」です。

X