🙄

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 を持ち、メンバーとして登録されたパッケージはそれを共有します。

https://docs.astral.sh/uv/concepts/projects/workspaces/

root/
├── .venv/ 共有の仮想環境
├── pyproject.toml [tool.uv.workspace] でメンバーを管理
└── dev/
    └── backend/
        └── pyproject.toml メンバーとして登録されている

これが便利なのは、パッケージ同士が互いに依存し合っている場合です。たとえば backend が社内の共通ライブラリ shared に依存しているようなケースでは、ワークスペースにまとめることで依存関係の解決がシンプルになります。

root/
├── shared/ 共通ライブラリ
└── backend/ shared に依存している

ただし今回やりたかったのはそういう構成ではありませんでした。backendmain.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 化する: 各ディレクトリがそのままデプロイ単位であり、他のディレクトリと依存関係がない
  • 環境を混ぜたくない: backendmain.py で必要なライブラリが全く異なり、依存解決を別々に行いたい
  • CI/CD の最適化: 特定のサービスが更新されたとき、そのディレクトリの uv.lock だけを見てビルドを回したい

そもそもPythonのことをまだまだ理解できていない部分も多いので、ツールに振り回されないようにしないといけないなと感じました。

Discussion