試しに、私のリポジトリのルートで 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 を使ってください。
PARTS LIST /目次
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 にしています。
ルートの値を上げた理由は、落とし穴の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回の作業です。
定期実行しているスクリプトの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 の入門記事の多くは「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 に向かない、と書かれています。
私のリポジトリは、自分しか使わない自動化スクリプトの集まりです。 全部が同じ 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プロジェクト分を毎回作り直す重さとどう折り合うか、まだ決めきれていません。
すでに uv workspace を使っているなら、消える予定のパッケージが表示されないかを確かめてください。まだ requirements.txt なら、1つのプロジェクトで uv add -r requirements.txt を試すところからで十分です。
続きを追う:X(@Tom_Java_Tech) / RSS
途中経過は、また書きます。

