本ページはプロモーションが含まれています

Python

uv workspaceでモノレポ管理|uv syncの落とし穴3つ

CATEGORY
REV.
DRAWN BY
TOM
SHEETS
7枚
uv workspace(1つの.venvにまとめた落とし穴3つ)

トム

SESで約8年、環境を変えられないもどかしさの中にいました。32歳で自社開発企業に転職し、今はClaude Codeと自動化で、仕事も時間も組み直している途中です。
・Javaバックエンドエンジニア(Java歴12年以上)
・資格:基本情報技術者/応用情報技術者/Java Silver/Python3エンジニア認定基礎
詳細なプロフィール

𝕏

試しに、私のリポジトリのルートで uv sync --dry-run を打ってみました。 返ってきたのは「96個のパッケージをアンインストールします」という予告です。

--dry-run なので実害はありません。 ただ、付けずに打っていたら、毎日動いている自動化スクリプトの大半が止まるところでした。

私は個人の自動化スクリプト16本を、1つのリポジトリで管理しています。 pip と requirements.txt で回していた構成を、2段階で uv workspace の1つの .venv にまとめました。 uv workspace の使い方と移行の手順、そして実際に踏んだ落とし穴3つを、設定ファイルの実物付きで書きます。

この記事でわかること

  • uv workspace の仕組みと [tool.uv.workspace] の書き方
  • requirements.txt から uv workspace へ移行する5ステップ
  • uv sync で依存が消える・uv lock が止まるなど、実際にハマった落とし穴3つ
  • 1つの .venv にまとめて失ったものと、workspace が向かないケース

結論:uv workspace はルートの pyproject.toml に [tool.uv.workspace] を書くだけで始められます。ただし、ルートで同期するときは 必ず uv sync --all-packages を使ってください。

uv workspaceとは?1つのuv.lockと.venvで全体を管理する仕組み

uv workspace とは、1つのリポジトリにある複数の Python プロジェクトを、1つの uv.lock でまとめて管理する uv の機能です。 各プロジェクトは自分の pyproject.toml を持ったまま、依存の解決だけを全体で1回にします。

uv の公式ドキュメントでは Rust の Cargo の仕組みにならったものと説明されていて、メンバー全員で1つのロックファイルを共有します。 ルートで同期すれば、ルートの .venv 1つに全メンバーの依存が入ります。

つまり「どのプロジェクトの .venv を使うんだっけ」を考えなくてよくなる、というのがいちばん大きな変化です。 私の場合、移行前は .venv が14個ありました。

[tool.uv.workspace]のmembersとexcludeの書き方

workspace は、ルートの pyproject.toml に [tool.uv.workspace] の表を足すだけで作れます。 下は私のリポジトリのルートの設定から、workspace に関係する部分だけを抜き出したものです。

[project]
name = "allinone-storage"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
    "pytest>=7.0",
    "requests==2.33.1",
]

[tool.uv]
package = false

[tool.uv.workspace]
members = ["python/*"]

members は必須で、グロブ(python/* のようなワイルドカード)で書けます。 外したいディレクトリがあるときは、exclude = ["python/old_tool"] のように任意の exclude を足します。

package = false は「このプロジェクトはパッケージとしてビルドしない」という指定です。 スクリプトを置いているだけのフォルダなら、ルートにも各メンバーにも付けておくと、ビルド設定([build-system])を書かずに済みます。

requires-pythonは全メンバーの共通部分になる

workspace 全体の Python のバージョンは、全メンバーの requires-python の共通部分(交わり)で1つに決まります。 メンバーごとに別の Python を使い分けることはできません。

たとえば、メンバーが >=3.10 で、ルートが >=3.11 なら、全体は >=3.11 になります。 私のリポジトリでは、各メンバーは >=3.10 のまま、ルートだけ >=3.11 にしています。

書く場所requires-python役割
各メンバー>=3.10そのプロジェクトが動く下限
ルート>=3.11統合 .venv の Python に合わせ、全体の下限を決める
workspace 全体>=3.11(共通部分)uv.lock の解決に使われる

ルートの値を上げた理由は、落とし穴の3つめで書きます。

requirements.txt時代から2段階でuv workspaceへ移った経緯

最初から uv workspace を狙っていたわけではありません。 まず pip から uv へ道具を替え、数か月後に .venv を1つにまとめる、という2段階で移りました。 いきなり全部を変えなかったのは、毎日動いている定期実行を止めたくなかったからです。

flowchart LR
    A["pip + requirements.txt<br>.venv はプロジェクトごと"] -->|"1段目"| B["pyproject.toml + uv.lock<br>.venv はまだプロジェクトごと"]
    B -->|"2段目"| C["uv workspace<br>uv.lock も .venv も1つ"]

プロジェクトごとのrequirements.txtが限界だった理由

2026年の春までは、各プロジェクトに requirements.txt と .venv がありました。 バージョンは、リポジトリ共通の constraints.txt でそろえていました。 インストールは pip install -r requirements.txt -c ../../constraints.txt を、プロジェクトの数だけ打つ方式です。

この方式のつらさは、次の3つでした。

  • requests の版を上げると、constraints.txt と全プロジェクトの .venv を1つずつ入れ直す
  • requirements.txt には直接入れたものしか書かれず、依存の依存の版は固定されない
  • 親のシェルに残った VIRTUAL_ENV のせいで、別のプロジェクトの .venv に入れてしまう事故が起きうる

3つめは地味ですが、自動化スクリプトを一括で入れ直す処理を書いていると、真っ先に気になる部分です。

1段目:各プロジェクトをpyproject.tomlとuv.lockへ

1段目では、14プロジェクトすべてで requirements.txt と constraints.txt を消し、pyproject.toml と uv.lock に置き換えました。 .venv はまだプロジェクトごとに残しています。

RSS 監視のプロジェクトなら、requirements.txt に書いていたライブラリが、そのまま dependencies に移るだけです。 下はいまの形です。

[project]
name = "feed"
version = "0.1.0"
requires-python = ">=3.10"
dependencies = [
    "feedparser==6.0.12",
]

[tool.uv]
package = false

この段階で、依存の依存まで uv.lock に固定されるようになりました。 一方で、uv.lock が14個に増え、requests の版を上げるときは相変わらず14回の作業です。

LOG1段目と2段目の間依存の書き漏れで定期実行が落ちた

定期実行しているスクリプトの1本が、ある日 ModuleNotFoundError: No module named 'dotenv' で止まりました。原因は pyproject.toml に python-dotenv を書き忘れていたこと。足して uv sync したら直りましたが、日記には「pyproject.toml と実コードの依存不整合は今後も気をつけたい」と書いています。この反省が、あとで別の形で返ってきます。

2段目:ルートにworkspaceを作り.venvを1つにする

2段目では、ルートの pyproject.toml に [tool.uv.workspace] を足し、各プロジェクトを workspace のメンバーにしました。 同時に、定期実行(macOS の launchd)の設定ファイル約20本で、Python のパスをルートの .venv に書き換えています。

変更前: /Users/.../リポジトリ/python/各プロジェクト/.venv/bin/python3
変更後: /Users/.../リポジトリ/.venv/bin/python3

各プロジェクトに残っていた個別の uv.lock は、少しあとでまとめて削除しました。 uv workspace ではルートの uv.lock だけが使われるので、残しておいても混乱のもとにしかなりません。

いまは16プロジェクトの依存が、3,187行の uv.lock 1つと、1.7GBの .venv 1つに収まっています。

requirements.txtからuv workspaceへ移行する5ステップ

ここからは、私の移行を一般的な手順に直したものです。 1段目と2段目を一度にやる場合でも、順番は同じです。 動作は uv 0.8.4 で確かめています。

step
1
ルートのpyproject.tomlに[tool.uv.workspace]を書く

前の章の設定を参考に、members にプロジェクトのフォルダをグロブで書きます。 requires-python はルートの .venv で使う Python に合わせておくと、あとで困りません。

step
2
各プロジェクトのrequirements.txtをpyproject.tomlへ移す

各プロジェクトのフォルダに pyproject.toml を作り、依存を dependencies に書きます。 手で写してもいいですし、そのフォルダで uv add -r requirements.txt を実行すれば、requirements.txt の中身をまとめて取り込めます。

step
3
ルートでuv lockを実行する

ルートで uv lock を打つと、全メンバーの依存を一度に解決して uv.lock を1つ作ります。 版がぶつかるメンバーがあると、ここでエラーになります。

step
4
ルートでuv sync --all-packagesを実行する

uv sync --all-packages で、全メンバーの依存をルートの .venv に入れます。 --all-packages を付け忘れると何が起きるかは、次の章の1つめの落とし穴で書きます。

step
5
定期実行のパスを差し替え、古いファイルを消す

cron や launchd、シェルスクリプトから呼んでいる Python のパスを、ルートの .venv/bin/python に書き換えます。 すべてのジョブが動いたのを確かめてから、個別の .venv・uv.lock・requirements.txt を削除します。

補足
launchd や cron は、ログインシェルの PATH を読みません。uv run ではなく、.venv/bin/python の絶対パスを直接書くのが確実です。launchd の基本は Mac launchdの使い方 にまとめています。

uv workspaceで踏みやすい3つの落とし穴

移行の作業そのものは、設定ファイルの書き換えが中心です。 厄介なのは、移したあとに出てくる落とし穴のほうでした。 2つめと3つめは、移行から数か月たって私が実際に手を入れたもので、1つめはその対策を入れた理由です。

--all-packagesなしのuv syncでメンバーの依存が消える

ルートで --all-packages を付けずに uv sync を打つと、メンバーの依存がルートの .venv から消えます。 uv の公式ドキュメントにも、uv sync と uv run は既定でワークスペースのルートだけを対象にする、と書かれています。

しかも uv sync は、ロックファイルにないパッケージを .venv から取り除きます。 ルート自身の依存は pytest と requests だけなので、ほかは全部「余計なもの」と判定されるわけです。

私のリポジトリで試すと、こうなりました。

uv sync --dry-run
# Would uninstall 96 packages

uv sync --all-packages --dry-run
# Audited 107 packages
# Would make no changes

107個のうち96個が消える計算です。 消える一覧には Playwright・feedparser・python-dotenv も入っていて、定期実行はほぼ全滅します。

ハマりポイント
ルートでは uv sync ではなく uv sync --all-packages

uv の入門記事の多くは「uv sync で環境がそろう」と書いています。単体のプロジェクトならそのとおりですが、workspace のルートでは逆に依存を消すコマンドになります。私は一括セットアップのスクリプトと Makefile に --all-packages を書き込み、手で打たないようにしました。迷ったら、まず --dry-run を付けて確かめてください。

pyproject.tomlの無いディレクトリでuv lockが止まる

members に書いたグロブに、pyproject.toml のないフォルダが1つでも引っかかると、uv lock も uv sync も止まります。 公式ドキュメントでも、members に当たるフォルダは必ず pyproject.toml を持つ決まりです。

手元で再現すると、次のエラーが出ます。

error: Workspace member `.../python/leftover` is missing a `pyproject.toml` (matches: `python/*`)

厄介なのは、git に何も残っていない古いフォルダでも引っかかる点です。 使わなくなったスクリプトを消しても、ログファイルだけが残ったフォルダがローカルにあると、そこで止まります。

最初は exclude に該当するフォルダを書き足していましたが、増えるたびに書くのは面倒でした。 いまは、pyproject.toml のないフォルダを探すだけの小さなチェックを pre-commit に入れています。

from pathlib import Path

# members = ["python/*"] に当たるフォルダのうち、pyproject.toml が無いものを探す
missing = [
    d for d in sorted(Path("python").iterdir())
    if d.is_dir() and not d.name.startswith((".", "__")) and not (d / "pyproject.toml").is_file()
]
if missing:
    print("pyproject.toml が無いフォルダ:", *missing, sep="\n  ")
    raise SystemExit(1)

requires-pythonの下限が低いとuv.lockが膨らむ

3つめは、止まりはしないものの、気づくと uv.lock が無駄に大きくなっている落とし穴です。 workspace 全体の requires-python が >=3.10 のままだと、uv は 3.10 で動く組み合わせも探して uv.lock に書き込みます。

私の .venv は Python 3.11 なので、3.10 向けの解決は使われません。 ルートの requires-python を >=3.11 に上げたところ、uv.lock の差分は316行の削除と49行の追加でした。

Before:ルートも >=3.10

3.10 向けと 3.11 以上向けの解決が両方 uv.lock に入る。使わない分岐の差分がレビューのたびに出る

After:ルートだけ >=3.11

解決は 3.11 以上の1通り。uv.lock は316行減って49行増え、差分も読みやすくなった

各メンバーの requires-python は >=3.10 のまま残しています。 リンターや型チェックの対象は 3.10 に合わせているので、メンバー側は「このコードが動く下限」、ルート側は「実際に使う Python」と役割を分けました。

1つの.venvにまとめて失ったもの・向かないケース

.venv を1つにして、版の更新も入れ直しも1回で済むようになりました。 ただ、手放したものもあります。 良い面だけ書くと嘘になるので、こちらも正直に書きます。

依存の書き漏れが共有の.venvでは表に出ない

共有の .venv では、自分の pyproject.toml に書いていないパッケージも import できてしまいます。 ほかのメンバーが入れたものが、同じ .venv に入っているからです。

uv workspace の公式ドキュメントにも、この点の注意書きがあります。 あるメンバーがほかのメンバーの依存を import していないことを、uv は保証できません。 手元でも、依存を何も書いていないメンバーから、別のメンバーが入れた six を import できました。

ここで、1段目と2段目の間に落ちた定期実行を思い出してください。 あのときは .venv が別々だったので、書き漏れが ModuleNotFoundError としてすぐに出ました。 いまの構成なら、たまたまほかのメンバーが python-dotenv を入れていれば、何事もなく動いてしまいます。

動くのだから問題ない、と言いたいところです。 でも、そのメンバーを別の環境へ持ち出したり、ほかのメンバーが python-dotenv を外したりした瞬間に、突然壊れます。 私のリポジトリには、この穴を見張る仕組みがまだありません。

依存が衝突するプロジェクトはworkspaceに入れない

uv workspace は、全メンバーの依存を同時に入れられることが前提です。 公式ドキュメントでも、メンバー同士の依存がぶつかる場合や、メンバーごとに別の .venv がほしい場合は workspace に向かない、と書かれています。

状況uv workspace代わりの方法
全プロジェクトが同じ版で動く◎ 向いている—
1つだけ古いライブラリの版が要る× 解決できないそのプロジェクトだけ workspace から外し、単独の uv プロジェクトにする
メンバーごとに Python の版を変えたい× 1つに決まる別リポジトリか、単独の uv プロジェクトにする
1つのプロジェクトだけ Docker で配る△ 工夫が要るuv sync --package で対象を絞る

私のリポジトリは、自分しか使わない自動化スクリプトの集まりです。 全部が同じ requests の版で動けば十分なので、ぶつかる理由がそもそもありませんでした。 チームで別々にリリースするサービスを抱えているなら、プロジェクトごとの .venv のほうが合う場面も多いはずです。

Docker で Python を動かすときの uv の使い方は、DockerでPython環境構築の手順 にまとめています。

uv workspaceのよくある質問

uv workspace を使い始めるときに迷いやすい点を、6つの質問にまとめました。 どれも、本文で扱った移行と落とし穴のどこかに関係しています。 回答の数値やコマンドは、uv 0.8.4 で確かめた内容です。

Q1. uv workspaceとは何ですか?

A1. 1つのリポジトリにある複数の Python プロジェクトを、1つの uv.lock でまとめて管理する uv の機能です。ルートの pyproject.toml に [tool.uv.workspace] を書き、members にプロジェクトのフォルダを指定すると使えます。

Q2. uv syncとuv sync --all-packagesの違いは?

A2. uv sync はワークスペースのルートの依存だけを .venv に入れ、それ以外は取り除きます。uv sync --all-packages は全メンバーの依存を入れます。ルートの .venv を全メンバーで共有するなら、--all-packages を付けてください。

Q3. pyproject.tomlの無いフォルダがあるとどうなりますか?

A3. members のグロブに当たったフォルダに pyproject.toml がないと、uv lock と uv sync が「is missing a pyproject.toml」のエラーで止まります。そのフォルダを消すか、exclude に書いて外してください。

Q4. メンバーごとに違うPythonのバージョンを使えますか?

A4. 使えません。workspace 全体の Python のバージョンは、全メンバーの requires-python の共通部分で1つに決まります。別の版が要るプロジェクトは、workspace から外して単独のプロジェクトにします。

Q5. cronやlaunchdからはどのPythonを指定すればいいですか?

A5. ルートの .venv/bin/python を絶対パスで指定します。定期実行はログインシェルの PATH を読まないことが多く、uv コマンド自体が見つからない場合があるためです。

Q6. requirements.txtはpyproject.tomlにどう移せばいいですか?

A6. 各プロジェクトのフォルダに pyproject.toml を作り、そのフォルダで uv add -r requirements.txt を実行すると、requirements.txt の中身が dependencies に入ります。最後にルートで uv lock と uv sync --all-packages を実行します。

まとめ:uv workspaceで.venvを1つにして変わったこと

uv workspace にして、14個あった .venv が1つになり、ライブラリの版の更新も入れ直しも1回で済むようになりました。 冒頭の96個のパッケージも、--all-packages を書き込んだスクリプトから同期している限り、消えることはありません。

【要点まとめ】

  • uv workspace は、ルートの pyproject.toml に [tool.uv.workspace] と members を書くだけで始められる
  • ルートで同期するときは、必ず uv sync --all-packages を使う
  • members に当たるフォルダには、必ず pyproject.toml を置く
  • ルートの requires-python は、実際に使う Python に合わせる
  • 共有の .venv では依存の書き漏れが表に出ないので、そこだけは別の手段で見張る

最後の1つは、私もまだ答えを持っていません。 各メンバーを単独の .venv で動かすテストを、ときどき回すのが筋だとは考えています。 ただ、16プロジェクト分を毎回作り直す重さとどう折り合うか、まだ決めきれていません。

NEXT STEP
いまのリポジトリで uv sync --dry-run を1回打ってみる。

すでに uv workspace を使っているなら、消える予定のパッケージが表示されないかを確かめてください。まだ requirements.txt なら、1つのプロジェクトで uv add -r requirements.txt を試すところからで十分です。

途中経過は、また書きます。

NEXT READ本棚から1本

AIで開発を組み直すなら、まずこの5冊から

入門・実践・設計まで、Claude Codeの本をレベル別に5冊選んで比べました。

→ おすすめ5冊を見る

  • この記事を書いた人
  • 最新記事

トム

SESで約8年、環境を変えられないもどかしさの中にいました。32歳で自社開発企業に転職し、今はClaude Codeと自動化で、仕事も時間も組み直している途中です。
・Javaバックエンドエンジニア(Java歴12年以上)
・資格:基本情報技術者/応用情報技術者/Java Silver/Python3エンジニア認定基礎
詳細なプロフィール

𝕏

Python