requirements.txt に依存パッケージを書くとき、flask の後ろに何を書くかで迷ったことがある人は多いはずだ。何も付けない、==3.0.0 で固定する、>=3.0.0 で下限だけ決める、~=3.0.0 で中間を取る——見た目は似ているが、それぞれが将来のインストール結果に対して全く違う約束をしている。今回は pip のバージョン指定記法を整理した上で、自社ツールの requirements.txt が実際にどの記法を選び、それによって何を得て何を引き換えにしているかを見ていく。
バージョン指定記法が答えている問い
補足: pip のバージョン指定は PEP 440 という仕様に従っている。パッケージ名の後ろに演算子とバージョン番号を書くことで、「インストールしてよいバージョンの範囲」を pip に伝える仕組みだ。
バージョン指定を何も書かなければ、pip はそのパッケージの最新版を無条件にインストールする。これは「今この瞬間の最新版で構わない」という意思表示だが、同じ requirements.txt を半年後に別の環境で pip install すると、当然ながら半年分新しくなった別のバージョンが入る。バージョン指定記法は、この「将来どのバージョンが入るか」という不確実性に、どれだけ制約をかけるかを選ぶための道具だ。
主要な演算子の違い
| 記法 | 意味 |
|---|---|
==3.0.0 |
このバージョンだけを許可する(完全固定) |
>=3.0.0 |
このバージョン以上ならすべて許可する(下限のみ) |
<3.0.0 |
このバージョン未満のみ許可する(上限のみ) |
!=3.0.0 |
このバージョンだけを除外する |
~=3.0.0 |
3.0.0 以上・3.1.0 未満を許可する(互換リリース演算子) |
このうち一番誤解されやすいのが ~=(互換リリース演算子)だ。~=3.0.0 は「>=3.0.0, ==3.0.*」と等価で、つまりパッチバージョン(3番目の数字)の更新は受け入れつつ、マイナーバージョン(2番目の数字)が上がった時点で締め出す。~=3.0 のようにマイナーバージョンまでしか書かなければ、今度はマイナー更新まで許容範囲が広がり「>=3.0, ==3.*」相当になる。どこまでの桁を書くかで許容範囲の粒度が変わる点が、単純な >= との違いだ。
自社ツールの requirements.txt — なぜ >= を選んでいるか
自社のデスクトップ保守ツールの requirements.txt は、6 つの依存パッケージすべてを >= の下限指定だけで書いている。
flask>=3.0.0
fabric>=3.2.0
playwright>=1.40.0
cryptography>=41.0.0
Pillow>=10.0.0
certifi>=2024.0.0
== で完全固定せず >= にしているのは、セキュリティ修正やバグ修正を含む新しいバージョンを自動的に取り込めるようにするためだ。特に cryptography(暗号処理)や certifi(CA証明書バンドル)は、脆弱性修正やルート証明書の更新が定期的に入るパッケージであり、古いバージョンに固定し続けること自体がリスクになりうる。上限を指定していないのも同じ理由で、将来出るバージョンをあらかじめ排除しない設計だ。
>= が引き換えにしているもの — ビルド再現性
一方で >= には代償がある。このツールは arch -x86_64 pip3 install -r requirements.txt && arch -x86_64 python3 build_app.py という手順で PyInstaller によりバイナリへパッケージングして配布している。requirements.txt の中身を一切変更していなくても、このコマンドを数週間空けて 2 回実行すれば、それぞれの実行時点で「3.0.0 以上」の条件を満たす最新版が解決されるため、実際にインストールされる flask や cryptography のバージョンが違う可能性がある。つまり requirements.txt の差分だけを見ても、2 つのビルドが本当に同じ依存関係で作られたかどうかは分からない。
この不確実性が実害になるのは、あるバージョンだけで再現する依存パッケージ側の不具合や、マイナーバージョンアップに伴う挙動変更(deprecation warning が error に格上げされる、デフォルト値が変わる等)に、ビルドのたびに気付かないまま巻き込まれるケースだ。== で完全固定していれば「更新するときだけ意図的にバージョンを上げて動作確認する」という運用にできるが、>= のままだと更新のタイミングが pip install を実行した瞬間に暗黙的に決まってしまう。
再現性を厳密に取りたいプロジェクトでは、requirements.txt とは別に pip freeze の出力をロックファイルとして保存し、通常のインストールはロックファイル経由・依存の意図的な更新だけ requirements.txt の緩い指定から再生成する、という 2 段構えを取ることが多い。自社ツールは少人数体制でビルド環境がほぼ固定されており、依存パッケージの更新頻度も高くないため、現時点ではこの 2 段構えまでは導入せず >= 一本で運用している。ビルド環境を増やす、あるいはビルドのたびに依存バージョンが微妙に変わることが実際に問題を起こすようになれば、ロックファイル導入を検討する余地がある判断だ。
まとめ
==・>=・~= はどれも「将来インストールされるバージョンをどこまで制約するか」という同じ問いに対する異なる答えだ。== は再現性を最大化する代わりに更新を手動化し、>= は更新を自動的に取り込める代わりに「今どのバージョンが入っているか」を requirements.txt の記述だけでは特定できなくする。自社ツールが >= を選んでいるのは、暗号・証明書関連パッケージの更新を取りこぼしたくないという理由が大きいが、これは「再現性を諦めてよい」という判断とセットになっている点を忘れてはならない。どの記法を選ぶかは、そのプロジェクトが再現性と最新性のどちらを優先するかという設計判断そのものだ。