uv と direnv を用いて Python 環境をプロジェクトごとに動的に切り替える

## 1. はじめに

チーム開発を進める中で、各開発者の Python バージョンやインストールされているパッケージが異なり、開発環境の統一が難しいという問題に直面することがあります。この問題を解決し、Python のバージョン管理だけでなく、使用するライブラリ群のバージョンもプロジェクトごとに統一し、かつディレクトリ移動に応じて自動的に開発環境を切り替える方法を、`uv` と `direnv` を用いて実現します。

## 2. 前提

本記事の手順は、以下の環境を前提としています。

- macOS であること
- Homebrew が利用可能であること

## 3. 本題

### 3-1. 必要なツールのインストール

まず、Python パッケージインストーラー兼バージョマネージャーである `uv` と、ディレクトリベースで環境変数を管理する `direnv` を インストールします。

```bash
brew install uv
brew install direnv
```

### 3-2. direnv をシェルに設定

`direnv` を利用するためには、お使いのシェルに応じた設定をシェルの設定ファイル（例: `.zshrc`, `.bashrc` など）に追加します。

**zsh の場合 (.zshrc):**

```bash
echo 'eval "$(direnv hook zsh)"' >> ~/.zshrc
source ~/.zshrc
```

**bash の場合 (.bashrc):**

```bash
echo 'eval "$(direnv hook bash)"' >> ~/.bashrc
source ~/.bashrc
```

設定後、ターミナルを再起動するか、`source` コマンドで設定を読み込んでください。

### 3-3. プロジェクトごとの環境構築

ここでは、例として `sample_project` という名前のプロジェクトを作成し、Python 3.13.3 の環境を構築します。

#### プロジェクトディレクトリの作成と `uv init` による初期化

まずプロジェクトディレクトリを作成し、`uv init` コマンドでプロジェクトを初期化します。これにより、`pyproject.toml` ファイルが生成されます。

```bash
mkdir sample_project
cd sample_project
uv init
```

`uv init` を実行することで以下ファイルが生成されます。
_注意: `uv init` が生成する内容は `uv` のバージョンによって変わることがあります。下記は一例です。_

![スクリーンショット 2025-05-31 15.18.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524922/d00fd1bc-4d3b-4161-b505-9216b1351866.png)

生成される `pyproject.toml` の例:

```toml:pyproject.toml
[project]
name = "sample-project"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12.2"
dependencies = []
```

#### `pyproject.toml` の編集

次に、`pyproject.toml` を編集して、プロジェクトに必要な依存関係を定義します。

例として、`fastapi` と `uvicorn` を通常依存、`ruff` と `pytest` を開発用依存として追加します。

```toml
[project]
name = "sample_project"
version = "0.1.0"
description = "A FastAPI project managed with uv and direnv."
readme = "README.md"
requires-python = ">=3.12.2"
dependencies = [
    "fastapi>=0.100.0",
    "uvicorn[standard]>=0.20.0",
]

# 開発用依存関係 (uv がサポートする形式)
[project.optional-dependencies]
dev = [
    "ruff",
    "pytest",
    "black",
]
```

_`requires-python` は、このプロジェクトが要求する Python のバージョンを指定します。使用する Python バージョンに合わせて適切に設定してください。_

`pyproject.toml` が編集できたらライブラリを同期します。

```bash
uv pip sync pyproject.toml
```

ライブラリがインストールされていることが確認できる
![スクリーンショット 2025-05-31 15.33.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524922/dc98f273-17b6-4e01-8e37-4281f984be11.png)



#### uv による仮想環境の作成

`uv venv` コマンドを使用して、プロジェクト用の仮想環境を作成します。`pyproject.toml` の `requires-python` で指定したバージョンと互換性のある Python バージョンを指定します（例: Python 3.13.3）。

```bash
uv venv --python 3.13.3
```

成功すると、プロジェクトディレクトリ直下に `.venv` という名前の仮想環境が作成されます。

![スクリーンショット 2025-05-31 15.18.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524922/1f384451-5237-4992-b5c3-45f3db0d5b13.png)


#### `.envrc` ファイルの作成と編集

次に、ディレクトリに入ったときに自動的に仮想環境が有効になり、依存関係が同期されるように `.envrc` ファイルを作成・編集します。

```bash
# 0. uv コマンドの存在確認
# `has` は direnv の標準関数で、コマンドの存在をチェックします。
if ! has uv; then
  echo "uv が見つからないためパッケージの同期をスキップします" >&2
  return 1 # .envrc の処理を中断し、エラーコード 1 を返すが、シェルは終了しない
fi

# 1. 仮想環境のアクティベート
# プロジェクトローカルの .venv を有効化します。
source .venv/bin/activate

# 2. pyproject.toml と uv.lock の変更を監視
# これらのファイルが変更された場合、direnv は .envrc を再ロードします。
watch_file pyproject.toml
watch_file uv.lock # uv.lock は uv pip sync/add などで生成・更新されます

# 3. 依存関係の自動同期
# uv.lock に変更があった場合のみ実行されます。
# 開発環境を想定し、開発用依存関係も含めて同期します (--extra-index-url dev)。
# `uv pip sync` は `pyproject.toml` と `uv.lock` (あれば) に基づいて環境を同期します。
if [ -f uv.lock ]; then
    uv pip sync pyproject.toml --extra-index-url dev
fi

```

上記の内容を `sample_project/.envrc` として保存します。

<details><summary>解説</summary>

- `has uv`: `uv` コマンドが利用可能かチェックします
- `source .venv/bin/activate`: `uv` で作成したローカルの仮想環境を有効にします
- `watch_file pyproject.toml` / `watch_file uv.lock`: これらのファイルが変更されると、`.envrc` が再評価され、後続のコマンド（ここでは `uv pip sync`）が再実行されるトリガーとなります
- `uv pip sync pyproject.toml --extras dev`: `pyproject.toml` に記述された通常依存と、`[project.optional-dependencies]` の `dev` グループに記述された開発用依存関係を仮想環境に同期する。`uv.lock` ファイルが存在すれば、ロックファイルに基づいて同期し、なければ `pyproject.toml` から解決して `uv.lock` を生成/更新する

</details>

#### direnv に設定の読み込みを許可

セキュリティのため、`direnv` は `.envrc` ファイルの内容を自動的には実行しません。
以下のコマンドで、このディレクトリの `.envrc` ファイルの実行を許可します。

```bash
direnv allow .
```

初回許可後、`sample_project` ディレクトリに入るたびに、自動的に `.venv` 仮想環境が有効になり、依存関係が `pyproject.toml` に基づいて同期されます。

![スクリーンショット 2025-05-31 15.26.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524922/0527f91b-2db9-4670-9806-bc7c011f1487.png)

#### 依存関係の確認

`.envrc` が正しく設定されていれば、`direnv allow .` を実行した際、または一度ディレクトリ外に出て再度入った際に、`uv pip sync` が実行され、`pyproject.toml` に記述した依存関係がインストールされます。

`uv pip list` でインストールされたパッケージを確認できます。

```bash
uv pip list
```

![スクリーンショット 2025-05-31 15.39.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524922/11c47707-b720-41a0-968c-440e15c37eaa.png)


## 4. 開発ワークフロー

### 4-1. 新しいパッケージの追加

プロジェクトに新しいパッケージを追加する場合は、`uv add` コマンドを使用します。これにより、`pyproject.toml` が自動的に更新され、`uv.lock` も更新されます。`direnv` が変更を検知し、自動的に `uv pip sync` を実行して仮想環境にパッケージをインストールします。

**通常依存の追加:**

```bash
uv add requests
```

`pyproject.toml` の `[project].dependencies` に `requests` が追加されます。

**開発用依存の追加 (optional-dependencies の 'dev' グループへ):**

```bash
uv add "ruff" --group dev
uv add "black" --optional dev
```

`pyproject.toml` の `[project.optional-dependencies.dev]` に `ruff` や `black` が追加されます。
(`uv add` の `--dev` や `--group` オプションの挙動は `uv` のバージョンにより若干異なる場合があるため、公式ドキュメントをご確認ください。`[project.optional-dependencies]` を使うのが標準的です。)

`uv add` 実行後、`.envrc` の `watch_file` 機能により `direnv` が `pyproject.toml` の変更を検知し、自動的に `uv pip sync` を実行して新しいパッケージが仮想環境にインストールされます。手動で同期したい場合は、ディレクトリに入り直すか、`direnv reload` を実行します。

### 4-2. 依存関係の更新

既存のパッケージを最新バージョンに更新したい場合も、`pyproject.toml` を直接編集するか、`uv add package_name@latest` のようにして更新後、`direnv` が自動で同期します。
もし手動ですべてのパッケージを最新バージョンに更新する場合は以下コマンドを実行します。

```bash
# すべてのパッケージを最新バージョンに更新
uv pip sync pyproject.toml --upgrade

# 特定のパッケージのみを更新
uv pip sync pyproject.toml --upgrade-package requests

# 開発用依存関係も含めて更新
uv pip sync pyproject.toml --upgrade --group=dev
```

## 5. 動作確認

設定が正しく行われていれば、以下の動作が確認できます。

### 5-1. `sample_project` へ移動

ターミナルで `sample_project` ディレクトリに移動すると、`.envrc` がロードされ、`uv pip sync` が実行されます。プロンプトに `(.venv)` が表示され、仮想環境が有効になります。
    `python --version` や `which python` で仮想環境の Python が使われていることを確認します。
    `pyproject.toml` に記述した `fastapi` や開発用ツールの `ruff` などが `import` できるか（またはコマンドとして利用できるか）確認します。

![スクリーンショット 2025-05-31 16.00.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524922/42ff34d3-d10f-49d1-8b86-589a11c1f9ae.png)


### 5-2. `sample_project` から移動:

他のディレクトリに移動すると、`.envrc` がアンロードされ、仮想環境が無効になります。

![スクリーンショット 2025-05-31 15.59.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524922/faa84d1a-9f3c-4066-96ad-9adfaabdbe81.png)


### 5-3. `pyproject.toml` の変更:

`pyproject.toml` の依存関係を手動で変更・保存すると、`direnv` が変更を検知し、再度 `uv pip sync` を実行して環境を更新します。

![スクリーンショット 2025-05-31 16.03.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524922/4db89061-bf48-4da7-8c3e-70af6e959597.png)


## 6. まとめ

`uv` と `direnv`用いてプロジェクトごとの Python 環境を動的に切り替える方法を紹介しました。
チームメンバー内における Python バージョンやライブラリのバージョン統一に向けて少しでも参考になれば幸いです。

## 7. 【推奨】 `.gitignore` の設定

プロジェクトの `.gitignore` ファイルには、`uv` が作成する仮想環境ディレクトリと `direnv` が使用する状態管理ディレクトリを追加しておくことを推奨します。


```gitignore
# uv
.venv/
.direnv/
```

## 参考

https://docs.astral.sh/uv/

https://zenn.dev/tabayashi/articles/52389e0d6c353a#4.-uv%E3%81%AE%E9%AB%98%E5%BA%A6%E3%81%AA%E6%A9%9F%E8%83%BD

https://qiita.com/naogify/items/94b7862a212887b2cdd8

https://qiita.com/kompiro/items/5fc46089247a56243a62

https://zenn.dev/nnn112358/scraps/0020c1132557eb