Python パッケージング #2 pip と requirements.txt — 伝統的ワークフローの限界
前回でプロジェクトごとに仮想環境を作りました。次に残るのは、その中に何をインストールしたかを記録して再現する問題です。伝統的な答えが pip と requirements.txt です。十数年にわたって Python エコシステムを支えてきた方式で、いまでもどこでも出会うので正確に知っておく必要がありますが、同時にこの方式の穴こそが pyproject.toml と uv が生まれた理由でもあります。今回はその両面を扱います。
pip install がすること #
pip install requests を実行すると、pip は PyPI(Python Package Index)でパッケージを見つけ、wheel(ビルド済みの zip 形式の配布物)をダウンロードして、仮想環境の site-packages に展開します。このとき requests が依存するパッケージ(urllib3、certifi など)も一緒にインストールされます。あなたが指定したものが直接依存、付いてきたものが推移的依存です。この区別が今回の核心の概念です。
バージョンは指定子で制御します。
requests==2.32.3 # ちょうどこのバージョン
requests>=2.31 # 以上
requests~=2.32.0 # 2.32.x の中で最新 (互換リリース)
requests # 何であれ最新pip list でインストール一覧を、pip show requests で特定パッケージのバージョンと依存関係を確認します。
requirements.txt — 一覧をファイルに #
依存関係をファイルに書いておけば、環境を作り直せます。
# requirements.txt
django==5.2.4
requests~=2.32.0
celerypip install -r requirements.txt新しいマシンでも、同僚のコンピューターでも、この 2 行で環境ができあがります。ここまでが教科書の図で、実務の図はここからです。
pip freeze の罠 — スナップショットは意図を消します #
「いまの環境をそのまま記録したい」への伝統的な答えが freeze です。
pip freeze > requirements.txt現在の site-packages のすべてのパッケージが正確なバージョンで出力されます。再現性は確保できますが、代償があります。
amqp==5.3.1
billiard==4.2.1
celery==5.5.3
click==8.2.1
kombu==5.5.4
vine==5.1.0
...この一覧からは、自分が何をインストールしたのかがわかりません。 celery を 1 つ入れただけなのに、推移的依存 5 つが同じ資格で並んでいます。時間が経つと 3 つの問題が育ちます。
- 削除ができません: celery を取り除いても、amqp や kombu が直接依存なのか残骸なのかファイルからは判断できず、一覧は増える一方です。
- アップグレードが怖くなります: どの行が意図(2.32 でなければならない)で、どの行が偶然(その日に入ったバージョン)なのか、区別がありません。
- プラットフォームが焼き付きます: macOS で freeze した一覧には macOS でだけ必要なパッケージが混ざり、Linux の CI ではそのまま動かないこともあります。
逆側の罠 — 固定しないとインストール日のくじ引きです #
freeze が嫌だからと、バージョンなしで celery と 1 行だけ書くと、逆側の穴に落ちます。インストールする日付によって違う環境ができあがります。 今日の CI は celery 5.5 を受け取り、来月の新人のノート PC は 5.6 を受け取ります。「自分のマシンでは動くのに」のかなりの部分がこの時差です。直接依存にバージョンを固定しても、推移的依存は相変わらず浮いているので、穴は完全には塞がりません。
まとめると、requirements.txt 1 つでは、意図の一覧と再現用のスナップショットという別々の 2 つの文書を兼ねることになり、どちらを選んでも反対側が崩れます。 伝統的ワークフローはこれをファイル 2 つ(requirements.in に意図、コンパイルされた requirements.txt にスナップショット)に分けて解きました(pip-tools)。この「宣言とロックの分離」こそが現代のツールの標準設計になります。このシリーズでは #3 の pyproject.toml が宣言を、#5 のロックファイルがスナップショットを担います。
constraints — バージョンだけを抑える補助ファイル #
伝統的ワークフローでもう 1 つ知っておくべき道具が constraints です。
pip install -r requirements.txt -c constraints.txtconstraints ファイルはインストールはさせず、インストールされるときのバージョンだけを制限します。「urllib3 はセキュリティ問題で 2.5 未満は禁止」のような組織レベルのルールを、プロジェクトごとの requirements と分けて置くときに使います。推移的依存のバージョンを抑えられる唯一の伝統的な手段でもあります。
それでも requirements.txt に出会う場所 #
限界を知っても、この形式は消えません。Docker のベースイメージのビルド、レガシーなデプロイスクリプト、一部の PaaS のデフォルト検出、協業相手のリポジトリで出会い続けます。読んで診断できる必要がある理由です。幸い、次世代のツールはこの形式と双方向に互換です(#4 で uv が requirements.txt をそのまま吸収するのを見ます)。
まとめ #
今回扱った内容です。
- pip は PyPI から wheel を受け取って site-packages に展開し、指定した直接依存と付いてくる推移的依存が一緒にインストールされます
- バージョン指定子は
==(固定)、>=(以上)、~=(互換リリース)が読めれば十分です pip freezeは再現性をくれる代わりに意図を消します。直接・推移的依存が区別なく混ざり、削除とアップグレードが難しくなります- バージョンを固定しないと、インストールの時点ごとに違う環境ができます。直接依存だけ固定しても推移的依存が浮いていて穴は残ります
- 根本原因は、ファイル 1 つが「意図」と「スナップショット」を兼ねることです。宣言とロックの分離が現代ツールの標準設計です
- constraints ファイルは、インストールせずバージョンだけを制限する補助手段です
次回(#3 pyproject.toml)では、「意図の一覧」が住むことになる標準の住まい、pyproject.toml を扱います。依存関係の宣言からツールの設定まで、プロジェクトのすべての情報が 1 つのファイルに集まっていく過程です。