uv init したら親の pyproject.toml が勝手に書き換わった話
はじめに
最近、複数サービスをまとめて管理するモノレポ構成を作っていました。
やりたかったことはシンプルで、以下のような構成でそれぞれを別の Docker イメージとしてデプロイしたかったです。
root/
├── main.py ← スクリプト実行環境
└── dev/
├── backend/ ← FastAPI など、独立した Docker イメージ
└── frontend/ ← Vite / React
Python 環境の管理にはuvを使うことにしました。backend/に移動して、何も考えずにuv initを叩きました。
このとき、ワークスペースという概念を完全に意識していませんでした。 それが後で地味に面倒なことになります。
何が起きたか
backend/ ディレクトリに移動して uv init を実行すると、こんなログが出ました。
$ cd dev/backend
$ uv init
Adding `backend` as member of workspace `/path/to/root`
Initialized project `backend`
正直、最初は気にしませんでした。「初期化できた」くらいにしか思っていませんでした。
異変に気づいたのは、ルートの pyproject.toml を開いたときです。
# root/pyproject.toml
[tool.uv.workspace]
members = ["dev/backend"]
勝手に書き換わっていました。
uv init を叩いたのは backend/ の中のはずなのに、なぜか親ディレクトリの pyproject.toml が変更されていました。uv は親ディレクトリを再帰的に探索し、pyproject.toml が見つかるとそのワークスペースのメンバーとして自動登録する仕様になっているためです。
「別の Docker イメージにしたい」という意図とは裏腹に、backend/ はルートのワークスペースに取り込まれた状態になっていました。
そもそも uv のワークスペースとは何か
ここで一度、uv のワークスペースという概念を整理しておきます。
ワークスペースとは、複数の Python パッケージを一つの仮想環境でまとめて管理する仕組みです。ルートに一つ .venv を持ち、メンバーとして登録されたパッケージはそれを共有します。
root/
├── .venv/ ← 共有の仮想環境
├── pyproject.toml ← [tool.uv.workspace] でメンバーを管理
└── dev/
└── backend/
└── pyproject.toml ← メンバーとして登録されている
これが便利なのは、パッケージ同士が互いに依存し合っている場合です。たとえば backend が社内の共通ライブラリ shared に依存しているようなケースでは、ワークスペースにまとめることで依存関係の解決がシンプルになります。
root/
├── shared/ ← 共通ライブラリ
└── backend/ ← shared に依存している
ただし今回やりたかったのはそういう構成ではありませんでした。backend と main.py の間に依存関係はなく、それぞれ完全に独立した Docker イメージとして動かしたかっただけです。
ワークスペースは便利な仕組みですが、今回のケースではむしろ余計なお世話でした。
--no-workspace で何が変わるか
「勝手にワークスペースに追加されたくない」という場合に使うのが、--no-workspace フラグです。これを使うと、uv の挙動が明確に変わります。
フラグなしの場合(デフォルト)
前述の通り、親ディレクトリに pyproject.toml があると、自動的に「その一員」として振る舞おうとします。
$ cd dev/backend
$ uv init
Adding `backend` as member of workspace `/path/to/root` # ← 親を探しに行く
Initialized project `backend`
--no-workspace をつけた場合
このフラグを付与すると、uv は親ディレクトリの探索をスキップし、そのディレクトリを独立したプロジェクトとして初期化します。
$ cd dev/backend
$ uv init --no-workspace
Initialized project `backend` # ← ワークスペースへの追加ログが出ない!
挙動の決定的な差
このフラグの有無による最大の違いは、誰がプロジェクトを管理するかにあります。
| 項目 | ワークスペース(デフォルト) | --no-workspace |
|---|---|---|
| ルートへの影響 |
root/pyproject.toml が書き換わる |
何も起きない |
| ロックファイル |
root/uv.lock に集約される |
backend/uv.lock が作られる |
| 仮想環境 |
root/.venv を共有する |
backend/.venv が作られる |
Docker イメージをビルドする際、ワークスペース構成だと「親にある uv.lock」や「他のメンバーのコード」までビルドコンテキストに含める必要が出てきます。一方、--no-workspace で初期化していれば、backend/ ディレクトリの中身だけでビルドが完結するため、イメージの軽量化や疎結合な管理が非常に楽になります。
まとめ
結局、uv を使う際にワークスペースにするべきか、独立させるべきかは、以下の観点があると思いました。
ワークスペース(デフォルト)が向いているケース
-
共通ライブラリがある:
shared/のコードをbackend/から直接参照したい -
一括管理したい: すべてのサービスの依存関係を一度に
uv syncで解決したい -
モノレポ全体で Python バージョンを統一している: 全体で一つの
.venvを使うことに抵抗がない
独立プロジェクト(--no-workspace)が向いているケース
- 個別に Docker 化する: 各ディレクトリがそのままデプロイ単位であり、他のディレクトリと依存関係がない
-
環境を混ぜたくない:
backendとmain.pyで必要なライブラリが全く異なり、依存解決を別々に行いたい -
CI/CD の最適化: 特定のサービスが更新されたとき、そのディレクトリの
uv.lockだけを見てビルドを回したい
そもそもPythonのことをまだまだ理解できていない部分も多いので、ツールに振り回されないようにしないといけないなと感じました。
Discussion