型チェッカ設定と 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。
静的解析はコードを実行せずに、こうした問題を すべての分岐について 捕まえます。単体テストの補完であり、代替ではありません。
ツール分担 — 何が何をするのか #
| ツール | 役割 | 備考 |
|---|---|---|
| ruff | linting + formatting + import 整列 | flake8 + black + isort 統合。Rust 製で非常に高速 |
| mypy | 型検査(リファレンス実装) | 最も歴史が長く、互換性が広い |
| pyright | 型検査(Microsoft) | mypy より速く、推論が強力。VS Code の Pylance が同じエンジン |
| pre-commit | ローカルのコミット段階で自動実行 | 上記ツールを束ねるフレームワーク |
| GitHub Actions | PR 段階で同じ検証を再実行 | ローカルを飛ばした変更をブロックする安全網 |
核心原則は ローカルと 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 ruffpyproject.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 . --fixuv run ruff format .検査とフォーマットが分かれている点が重要です。ruff check は「このコードは間違っている」を知らせ、ruff format は「このコードの見た目を統一する」を行います。
pyright — 型検査の設定 #
インストール #
uv add --dev pyrightpyproject.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設定ファイル #
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
- sqlalchemypyright は外部ライブラリの型 stub を見るために、そのライブラリが同じ環境にインストールされている必要があります。pre-commit は隔離された venv で hook を回すので additional_dependencies で明示します。
インストールと初回実行 #
uv run pre-commit installuv run pre-commit run --all-filesこれ以降、git commit するたびに変更されたファイルに対して hook が自動で実行されます。失敗すればコミットがブロックされます。
hook を一時的にスキップする #
緊急 hotfix のような例外的な状況では git commit --no-verify でスキップできます。ただし PR 段階で同じ検証が再びブロックするので、結局は修正する必要があります。
GitHub Actions — PR 段階の安全網 #
ローカル hook をスキップした変更、または別の環境で作られた変更を捕まえる第二の網です。
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 --frozen—uv.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 marker3 つの選択肢があります。
types-some_libが PyPI にあるか確認(uv add --dev types-some_lib)- 自分で stub を書く(
stubs/some_lib.pyi) - その import だけ無視(
# pyright: ignore[reportMissingTypeStubs])
pyright と mypy の結果が違う #
2 つのツールの推論アルゴリズムが違います。両方を厳密な基準として同時に運用するのではなく、片方を基準ツールに決め、もう片方は IDE 補助程度に使います。本書では pyright を基準にします。
Any の伝染
#
ある関数が Any を返すと、その結果を受け取るすべての箇所が Any になります。reportUnknownReturnType、reportAny のような strict ルールがこれを捕まえますが、出発点は「外部ライブラリの境界でだけ Any、自分のコード内部では Any 禁止」です。
ruff と formatter の衝突 #
ruff が lint と format を両方担当すれば衝突はありません。ただし外部の black を併用すると規則がずれることがあります。本書では ruff format ひとつに統一します。
pre-commit hook のバージョンが古い #
.pre-commit-config.yaml の rev: が固定されるので、定期的に上げる必要があります。
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・フォーマット・テストを通過したコードしか入らなくなります。
練習問題 #
- ruff の select 拡張 — 本章のデフォルトの
selectにN(命名)、D(docstring)を追加してみてください。最初はどんなエラーが一番多く出ますか?per-file-ignoresでテストフォルダだけ一部ルールを緩めるにはどう書きますか? - pyright executionEnvironments — 自身のプロジェクト(なければ第29章の TODO API)で 1 つのディレクトリだけ
typeCheckingMode = "strict"に上げてみてください。どんなエラーが一番多く出ますか?Optionalの見落とし、Anyの伝染、return 型の欠落のうち、どれが一番多いですか? - CI キャッシュ最適化 — 上の GitHub Actions workflow の lint-and-typecheck job と test job が毎回依存関係を新しくインストールしています。
astral-sh/setup-uv@v4のenable-cache: trueがどう動作するか確認し、uv.lockが変わっていない PR の 2 回目の run が 1 回目に対してどれだけ速くなるか比較してみてください。
次の章は logging と観測性 です。静的検証で「コードの正確性」をブロックしたら、次の段階は「運用中に何が起きているかを見えるようにする方法」です。