Python パッケージング #1 仮想環境 — 環境が壊れる仕組みと venv

読了 5分

確かに pip install したのに ModuleNotFoundError が出る。プロジェクト A を直そうとパッケージを上げたらプロジェクト B が壊れる。ネットで見たとおり sudo pip install したら、今度は OS のコマンドがおかしくなる。Python をしばらく使えば誰もが一度は通るこの混乱は、実力の問題ではなく構造の問題です。このシリーズはその構造を土台から整理する 7 本です。仮想環境と pip の仕組みから始めて、pyproject.toml、uv、依存関係のロック、PyPI への公開、チームの規約まで登っていきます。モダン Python 基礎を終えた方を基準にしており、テスト自動化シリーズで当たり前のように使っていた uv がどんな問題を解く道具なのかも、このシリーズで明らかになります。

import はどこを探すのか — sys.path と site-packages #

import requests が実行されると、Python は sys.path に並んだディレクトリを順番に探します。直接確認できます。

sys.path の確認
python -c "import sys; print('\n'.join(sys.path))"

リストの終わりのほうに .../site-packages が見えます。pip がパッケージをインストールする場所がまさにこのディレクトリで、インタープリター 1 つに site-packages も 1 つです。 問題はここから始まります。マシンのすべてのプロジェクトがインタープリター 1 つを共有すれば、site-packages も 1 つを共有することになります。

プロジェクト A が django==4.2 を使い、プロジェクト B が django==5.2 を使うなら、同じ site-packages に 2 つのバージョンを並べて置く方法はありません。B のために上げれば A が壊れます。「数日前まで動いていたプロジェクトが動かない」の正体はたいていこれです。別のプロジェクトをいじりながら、共有の倉庫の中身を変えてしまったのです。

システム Python は OS の部品です #

macOS と Linux には Python が最初から入っています。これはあなたのための Python ではなく、OS のツールが使う部品です。ここの site-packages を sudo pip install で変えると、パッケージマネージャー(apt、brew)が管理していたファイルと衝突し、最悪の場合 OS のユーティリティが動かなくなります。

そのため最近のディストリビューションは、そもそもブロックしています。Debian・Ubuntu 系でシステム Python への pip インストールを試みると、このエラーに出会います。

出力例
error: externally-managed-environment
× This environment is externally managed

PEP 668 が定めた保護の仕組みで、「この Python は OS のものだから、仮想環境を作って使うように」という意味です。回避するフラグ(--break-system-packages)もありますが、名前のとおりシステムを壊すという署名なので使いません。正解は次の節です。

venv — プロジェクトごとに分離された Python #

仮想環境は、プロジェクト専用の site-packages を持つ軽量な Python のコピーです。標準ライブラリの venv モジュールで作ります。

仮想環境の作成
cd my-project
python -m venv .venv

.venv ディレクトリができて、中をのぞくと構造はシンプルです。

フォルダ構造
.venv/
├── bin/            # python、pip の実行ファイル (Windows は Scripts/)
├── lib/
│   └── python3.13/
│       └── site-packages/   # このプロジェクト専用の倉庫
└── pyvenv.cfg      # 元のインタープリターの場所を記録

インタープリター全体をコピーするのではなく、元を指す薄い殻に空の site-packages を新しく付けたものです。だから作成が速く、ディスクの負担もインストールしたパッケージの分しか増えません。これでプロジェクト A と B はそれぞれの .venv を持つので、Django 4 と 5 が共存します。衝突の構造的な原因が消えたのです。

activate の正体 — PATH の操作にすぎません #

仮想環境を「有効にする」と表現しますが、実際に起きることは地味です。

有効化
source .venv/bin/activate
(.venv) $ which python
/Users/me/my-project/.venv/bin/python

activate スクリプトの仕事は、シェルの PATH の先頭に .venv/bin を差し込むことがすべてです。以後 pythonpip と打つと仮想環境側の実行ファイルが先に見つかるだけで、魔法のような切り替えはありません。deactivate は PATH を元に戻します。

この正体を知ると 2 つのことが付いてきます。第一に、activate なしでもパスを直接指定すれば仮想環境はそのまま使えます。

直接実行
.venv/bin/python main.py        # activate なしで実行
.venv/bin/pip install requests  # activate なしでインストール

cron や systemd のようにシェルの初期化がないところでは、この方式が標準です。第二に、「activate を忘れた」という事故は which python の 1 行で診断できます。いま見つかる python がどのパスなのかが、環境まわりのデバッグすべての最初の質問です。

守るべきルール 3 つ #

  1. プロジェクトごとに .venv を 1 つ: 置き場所はプロジェクトルートの .venv が事実上の標準です。ツール(VS Code、uv)がこの名前を自動で認識します。
  2. .venv はコミットしません: .gitignore に入れます。仮想環境には絶対パスが埋め込まれていて別のマシンでは再利用できず、再現は次回扱う依存関係の記録で行います。
  3. システム Python には何もインストールしません: グローバルに置きたいツールがあっても、別の方法があります(#4 で扱う uv tool がその答えです)。

まとめ #

今回扱った内容です。

  • importsys.path を順番に探し、pip は site-packages にインストールします。インタープリター 1 つに site-packages が 1 つなので、グローバルなインストールはプロジェクト同士の衝突を生みます
  • システム Python は OS の部品です。externally-managed-environment エラー(PEP 668)はその保護装置で、回避しません
  • venv はプロジェクト専用の site-packages を持つ薄い Python の殻です。python -m venv .venv で作り、プロジェクトごとに 1 つ置きます
  • activate は PATH の先頭に .venv/bin を差し込むだけです。.venv/bin/python を直接呼んでもよく、環境がおかしいときは which python から確認します
  • .venv はコミットしません。再現は依存関係の記録で行います

次回(#2 pip と requirements.txt)では、その依存関係の記録の伝統的な方法である pip と requirements.txt を扱います。うまく使う方法とともに、この方式がどこで限界にぶつかるのかが、以降の回を理解する鍵になります。

X