Virtual environment、pip、pyproject.tomlをPythonで使いこなす
1. 概要
同じマシン上で複数のPythonプロジェクトを扱うようになると、バージョン競合に直面することになります。プロジェクトAはrequests 2.28が必要でも、プロジェクトBはrequests 2.32が必要という具合です。この記事では、この問題を解決するための3つのツールを紹介します。venvで各プロジェクトの環境を分離し、pipでパッケージをインストール・管理し、pyproject.tomlで最新の標準に従ってプロジェクトを宣言します。この記事を読み終わった後には、きちんと構造化されたPythonプロジェクトを構築でき、editableモードでインストールでき、pytestでテストを実行できるようになっているはずです。
2. Virtual environment
Virtual environment(venv)とは、3つのものを含むディレクトリです:独立したPythonインタープリター(通常はsymlink)、独立したsite-packagesディレクトリ、そしてアクティベーションスクリプトです。venv内にインストールされたパッケージはそのディレクトリ内にのみ存在し、システムのPythonに影響を与えません。
2.1. venvの作成とアクティベート
venvモジュールはPython 3.3以降の標準ライブラリに含まれているので、追加でインストールする必要はありません:
mkdir shopcart && cd shopcart
python3 -m venv .venv
# Linux / macOS
source .venv/bin/activate
# Windows (PowerShell)
.venv\Scripts\Activate.ps1
python3 -m venv .venvコマンドはプロジェクトに.venvディレクトリを作成します。.venvと名付けるのは一般的な慣例で、ほとんどのIDEはこれを自動的に認識します。アクティベートした後、シェルのプロンプトには(.venv)というプレフィックスがつきます。この時点で、pythonとpipコマンドはvenvを指します。
venvから抜けるには、以下を実行します:
deactivate
2.2. どのインタープリターを使っているかの確認
このステップを飛ばす人が多いため、「インストールしたはずなのにModuleNotFoundErrorが出る」といったエラーの原因になることが多いです:
which python
python -c "import sys; print(sys.prefix); print(sys.prefix != sys.base_prefix)"
/home/lan/shopcart/.venv/bin/python
/home/lan/shopcart/.venv
True
sys.prefixは現在実行している環境のルートディレクトリです。sys.base_prefixは元のPythonのディレクトリです。この2つの値が異なれば、あなたはvenvの中にいます。
注意:
.venvディレクトリをGitにコミットしないでください。.venv/を.gitignoreに追加してください。venvは絶対パスとOSに依存しているため、別のマシンにコピーすると壊れることがほとんどです。正しい方法は、dependency宣言ファイルをコミットして、新しいマシンでvenvを再作成することです。
3. pipでのパッケージ管理
pipはデフォルトのパッケージインストーラーで、PyPIからパッケージを取得します。
3.1. 基本的なコマンド
python -m pip install --upgrade pip
python -m pip install "requests>=2.31,<3"
python -m pip list
python -m pip show requests
python -m pip uninstall -y requests
各コマンドの説明:
install --upgrade pipはpip自体をアップグレードします。古いバージョンのpipはpyproject.tomlを正しく読めないことがあります。"requests>=2.31,<3"はversion specifierです。シェルが<をリダイレクトとして解釈しないよう、クォートで囲む必要があります。listはインストール済みのパッケージを一覧表示します。showはバージョン、インストール位置、依存パッケージを含む詳細情報を表示します。
3.2. pipではなくpython -m pipを使うべき理由
単なるpipコマンドは、あなたが使用しているpythonとは異なるPythonを指すかもしれません。これはマシンに複数バージョンのPythonがあるときや、venvのアクティベートを忘れたときに発生しやすいです。python -m pipコマンドは常に正確なインタープリターのpipを実行するため、パッケージは確実に期待する環境にインストールされます。
3.3. requirements.txtとpip freeze
パッケージリストを共有する従来の方法はrequirements.txtを使うことです:
python -m pip freeze > requirements.txt
python -m pip install -r requirements.txt
certifi==2024.8.30
charset-normalizer==3.4.0
idna==3.10
requests==2.32.3
urllib3==2.2.3
pip freezeはvenv内のすべてのパッケージを、urllib3のような間接的な依存関係も含めて、正確なバージョンで記録します。これは実行環境を再現するのに適しています。しかし、どのパッケージが本当に必要で、どのパッケージが引き込まれているだけなのかは教えてくれません。また、プロジェクト自体の説明(名前、バージョン、Pythonの要件)も含まれていません。それはpyproject.tomlの役割です。
4. pyproject.tomlでのプロジェクト宣言
pyproject.tomlはPythonプロジェクトの標準設定ファイルです。これはPEP 518、PEP 621およびその他の関連するPEPで定義されています。このファイルはsetup.pyとsetup.cfgに代わり、pytestやruffなどの複数のツール設定をまとめています。
4.1. プロジェクト構造
私たちはsrc layoutを使用します。つまり、コードをsrc/に配置します。このレイアウトにより、テストは常にインストール済みのパッケージで実行され、ワーキングディレクトリから直接インポートされるのを避けられます:
shopcart/
├── pyproject.toml
├── src/
│ └── shopcart/
│ ├── __init__.py
│ ├── order.py
│ └── cli.py
└── tests/
└── test_order.py
4.2. 完全なpyproject.tomlファイル
[build-system]
requires = ["setuptools>=69"]
build-backend = "setuptools.build_meta"
[project]
name = "shopcart"
version = "0.1.0"
description = "Simple order total calculation"
requires-python = ">=3.11"
dependencies = [
"requests>=2.31,<3",
]
[project.optional-dependencies]
dev = [
"pytest>=8",
"ruff>=0.5",
]
[project.scripts]
shopcart = "shopcart.cli:main"
[tool.pytest.ini_options]
testpaths = ["tests"]
各セクションの意味:
[build-system]はプロジェクトをビルドするのに必要なツールをpipに伝えます。ここではsetuptoolsです。これをhatchlingまたはflit_coreに置き換えることができ、他の部分を変更する必要はありません。[project]は標準メタデータを含みます。requires-pythonは、サポートされていないPythonバージョンへのプロジェクトのインストールを防ぎます。dependenciesはプロジェクトを実行するために必須なパッケージです。ここではバージョン範囲(>=2.31,<3)を宣言すべきで、固定バージョン(==2.32.3)ではありません。[project.optional-dependencies]はオプションのパッケージをグループ化します。devグループは、エンドユーザーが必要としない開発ツールを含みます。[project.scripts]はターミナルにshopcartコマンドを作成します。このコマンドはshopcart.cliモジュール内のmain関数を呼び出します。[tool.pytest.ini_options]はpytestの設定です。すべてのツールは独自の[tool.<name>]namespace を持ちます。
src layoutではsetuptoolsがsrc/内のパッケージを自動的に検出するため、追加の宣言は必要ありません。
4.3. パッケージのコード
# src/shopcart/order.py
from dataclasses import dataclass, field
from decimal import Decimal
@dataclass(frozen=True)
class OrderLine:
product: str
unit_price: Decimal
quantity: int
@dataclass
class Order:
customer: str
lines: list[OrderLine] = field(default_factory=list)
def add(self, product: str, unit_price: str, quantity: int = 1) -> None:
if quantity <= 0:
raise ValueError("quantityは0より大きい必要があります")
self.lines.append(OrderLine(product, Decimal(unit_price), quantity))
def total(self) -> Decimal:
return sum((l.unit_price * l.quantity for l in self.lines), Decimal("0"))
OrderはOrderLineのリストを保持します。金額計算の丸め誤差を避けるためDecimalを使うので、unit_priceは文字列として渡されます。メソッドtotal()は各行の小計を合算し、初期値はDecimal("0")です。
# src/shopcart/cli.py
from shopcart.order import Order
def main() -> None:
order = Order(customer="田中太郎")
order.add("カフェオレ", "29000", 2)
order.add("バゲット", "25000")
print(f"顧客: {order.customer}")
print(f"合計: {order.total():,} VND")
src/shopcart/__init__.pyは空でもかまいません。
4.4. editableモードでのインストール
python -m pip install -e ".[dev]"
shopcart
顧客: 田中太郎
合計: 83,000 VND
フラグ-e(editable)はパッケージをソースコードにリンクし、site-packagesにコピーせずに済みます。そのため、order.pyを編集すると、再インストール不要で即座に変更が反映されます。".[dev]"の部分は、現在のディレクトリでプロジェクトをインストール時にdevグループも含める意味なので、pytestとruffも一緒にインストールされます。shopcartコマンドは[project.scripts]での宣言のおかげでターミナルで利用可能になります。
注意: zshなどの一部のシェルは
[...]をglobパターンとして解釈します。そのため、常に".[dev]"をクォートで囲む必要があります。
5. テストの作成と実行
# tests/test_order.py
from decimal import Decimal
import pytest
from shopcart.order import Order
def test_total_sums_all_lines():
order = Order(customer="鈴木花子")
order.add("アイスティー", "35000", 2)
order.add("カスタードプディング", "15000", 3)
assert order.total() == Decimal("115000")
def test_empty_order_has_zero_total():
assert Order(customer="田中太郎").total() == Decimal("0")
def test_add_rejects_non_positive_quantity():
order = Order(customer="山田美咲")
with pytest.raises(ValueError, match="quantity"):
order.add("ミネラルウォーター", "10000", 0)
python -m pytest -q
... [100%]
3 passed in 0.02s
テストはshopcart.orderをインストール済みのパッケージとしてインポートします。これは上のステップでpip install -eが必要な理由です。まだインストールしていなければ、src layoutはModuleNotFoundErrorを報告します。このエラーは意図的なもので、ユーザーがインストールする実際のものをテストするよう強制しています。3番目のテストはpytest.raisesを使って、無効な数量が“quantity”を含むメッセージで拒否されることを確認します。
6. バージョン固定: pyproject.tomlかrequirements.txtか?
これら2つのファイルは互いに排他的ではありません。異なる目的に役立ちます:
pyproject.toml |
ロックファイル (ピン止めされたrequirements.txt) |
|
|---|---|---|
| 目的 | プロジェクトが必要なものを説明 | 環境を正確に再現 |
| バージョン | 範囲 (>=2.31,<3) |
固定 (==2.32.3) |
| 用途 | ライブラリとアプリケーション | デプロイ対象のアプリケーション (サーバー、Docker) |
PyPIに公開されるライブラリの場合、pyproject.tomlだけが必要です。バージョンを固定すると、ライブラリのユーザーが競合に遭遇します。
サーバーで実行するアプリケーションの場合、ロックファイルを持つべきです。そうすれば、デプロイのたびに同じバージョンセットがインストールされます:
python -m pip install -e .
python -m pip freeze --exclude-editable > requirements.lock
デプロイ時には、ロックファイルから最初にインストールした後、プロジェクトをインストールします。pipが依存パッケージを自動的にアップグレードしないようにするためです:
python -m pip install -r requirements.lock
python -m pip install --no-deps .
ロックプロセスをもっとシンプルにしたい場合は、pip-tools(コマンドpip-compile pyproject.toml)またはuvが使えます。どちらもpyproject.tomlを直接読み込んでハッシュ付きのロックファイルを生成します。
7. よくあるエラー
sudo pip installでパッケージをインストール。 このコマンドはOSのパッケージを上書きし、システムツールを壊す可能性があります。多くの最新のLinuxディストリビューションはexternally-managed-environmentエラーで明示的にブロックしています。解決方法は常にvenvを使うことです。- venvのアクティベートを忘れたまま、インストールまたは実行する。セクション2.2のように
which pythonで確認するか、.venv/bin/python -m pip ...を直接実行して確認できます。 - 異なるPythonでvenvを作成・実行。 venvは作成したインタープリターに厳密に結合されています。Pythonをアップグレードするとき(例えば3.11から3.12へ)、
.venvを削除して再作成してください。 - プロジェクトディレクトリを移動または名前変更。
.venv/bin内のスクリプトは絶対パスを含むため、移動後は壊れます。venvを再作成する必要があります。 - **古い
setup.pyとpyproject.tomlを混在させ、**両方の場所に依存関係を宣言する。唯一の信頼できる情報源を選んでください。新しいプロジェクトでは、それは[project]テーブルです。 - 開発ツールを
dependenciesに入れる。pytestやruffはoptional-dependencies内に置くべきです(または、ツールがサポートしている場合は[dependency-groups])。エンドユーザーがそれらをインストールする必要がないようにするためです。
注意:
blackやhttpieのような独立したCLIツールを単にマシン全体で実行する場合は、venvを使うべきではありません。この場合、pipxがより適切です。各ツール用に独立したvenvを自動作成し、コマンドをPATHに追加します。
8. まとめ
この記事ではPythonプロジェクトの基盤となる3つのツールを説明してきました:
python -m venv .venvで各プロジェクトの依存関係を分離。python -m pipで使用中のインタープリターに正確にパッケージをインストール。pyproject.tomlでメタデータ、依存関係、オプショングループ、CLIコマンド、ツール設定を1つのファイルで宣言。
src layoutとpip install -e ".[dev]"とpytestを組み合わせることで、クリーンで、テストしやすく、共有しやすいプロジェクトが実現できます。安定したデプロイメントが必要なアプリケーションの場合は、pyproject.tomlの傍らにロックファイルを追加して、すべての環境が同じバージョンセットをインストールするようにしてください。