Python パッケージング #2 pip と requirements.txt — 伝統的ワークフローの限界

読了 5分

前回でプロジェクトごとに仮想環境を作りました。次に残るのは、その中に何をインストールしたかを記録して再現する問題です。伝統的な答えが 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
# requirements.txt
django==5.2.4
requests~=2.32.0
celery
インストール
pip 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 です。

constraints 付きインストール
pip install -r requirements.txt -c constraints.txt

constraints ファイルはインストールはさせず、インストールされるときのバージョンだけを制限します。「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 つのファイルに集まっていく過程です。

X