系列:开发环境管理
uv 安装和使用教程 讲了 uv 的基本用法:创建项目、加依赖、同步环境、管理 Python 版本。那篇覆盖了日常工作 80% 的场景。
这篇聊几个进阶话题——这些是我在实际项目中踩过坑之后发现真正提高效率的东西,希望你也用得上。
我本机跑的是 uv 0.11.x,下面聊到的功能在 0.10 之后基本都稳定了。
内联脚本:一个文件就是完整项目
Python 一直缺一个好用的”单文件脚本”方案。以前写个一次性脚本,要么污染全局环境,要么手动建 venv,要么用 pipx run(还得知道包名)。uv 把 PEP 723 的内联脚本元数据做得很好用——脚本自己声明依赖,uv 自动处理环境。
基础用法
在 .py 文件顶部用 TOML 声明依赖:
# /// script# requires-python = ">=3.12"# dependencies = [# "httpx",# "rich",# ]# ///
import httpxfrom rich import print
resp = httpx.get("https://httpbin.org/json")data = resp.json()print(data)然后直接跑:
uv run script.py第一次运行 uv 会自动创建隔离环境、安装依赖、缓存结果。第二次跑几乎瞬间启动,因为环境已经就绪。
开发工具也可以声明
脚本不止能声明运行依赖,还能声明开发工具:
# /// script# requires-python = ">=3.12"# dependencies = ["pandas"]# dev-dependencies = ["pytest", "ruff"]# ///然后:
uv run --dev ruff check script.py这个方案特别适合数据分析脚本、一次性迁移脚本、自动化运维脚本——你把它扔给同事,他装上 uv 就能直接跑,不需要 requirements.txt、不需要 README 里写安装步骤。
什么时候用内联脚本,什么时候用项目
| 场景 | 用 |
|---|---|
| 一次性数据处理、临时迁移、自动化脚本 | 内联脚本 |
| 需要测试、CI、多文件模块、部署到服务器 | 完整项目 |
我的习惯:超过 200 行或者被别的文件 import,就升级成项目。之前的脚本搬进 src/ 也很方便。
工作区(Workspace):管理多个相关包
如果你的项目拆成了多个包——比如核心库 + CLI + Web 服务——以前得每个目录单独配 venv、单独 lock,版本容易飘。uv workspace 就是解决这个问题的,类似 npm 的 workspace 或者 Cargo 的 workspace。
创建 workspace
顶层 pyproject.toml:
[tool.uv.workspace]members = ["packages/*"]
[tool.uv]dev-dependencies = ["pytest", "ruff"]目录结构:
my-project/├── pyproject.toml├── packages/│ ├── core/│ │ ├── pyproject.toml│ │ └── src/core/__init__.py│ ├── cli/│ │ ├── pyproject.toml│ │ └── src/cli/__init__.py│ └── web/│ ├── pyproject.toml│ └── src/web/__init__.py└── uv.lock子包的 pyproject.toml 各自声明自己的依赖,uv 会统一解析到根目录的一把 uv.lock。
workspace 互引
子包之间可以直接引用。假设 cli 依赖 core:
[project]name = "my-cli"dependencies = ["my-core"]
[tool.uv.sources]my-core = { workspace = true }workspace = true 告诉 uv 去 workspace 里找 my-core,而不是去 PyPI 下载。本地修改 core 的代码,cli 立刻看到,不需要任何 publish 步骤。
常用命令
uv workspace list # 列出当前 workspace 的所有成员uv workspace dir # 显示 workspace 根目录uv sync # 在根目录执行,会同步所有成员的依赖uv run -p packages/cli my-cli --help # 在指定包的环境里运行命令几个踩坑点
- 锁文件只有一把:workspace 所有包共享根目录的
uv.lock。如果两个子包声明了同一个包的不同版本约束,uv 会帮你解冲突,但这个文件必须提交到 Git。 uv sync在子目录也能跑:但实际用的还是根目录的锁文件。如果你在子目录uv add,uv 会自动更新根目录的pyproject.toml和锁文件。- 不需要把所有子包都 publish:多数内部包只通过 workspace 互引就行,只有公开 API 的包才需要发到 PyPI。
工具链管理:uv tool 的进阶用法
基础教程里讲的 uv tool install ruff 只是冰山一角。uv 0.11 之后 uv tool 能做更多。
从本地源码安装
团队内部的 CLI 工具,不需要先发到 PyPI 再安装:
uv tool install --from ./packages/cli my-cliuv 会从本地目录构建并安装,改完代码重新跑一次就更新。
固定 Python 版本
默认 uv tool install 会用当前激活的 Python。如果工具要求特定版本:
uv tool install ruff --python 3.12也可以在 uv.toml 里做全局配置:
[tool.uv]python-preference = "only-managed"列出和升级
uv tool list # 安装了哪些工具,各自什么版本uv tool upgrade ruff # 升级单个uv tool upgrade --all # 全部升级临时用一下但不安装
uv tool run ruff check .这个和 uvx 一样,uvx 是 uv tool run 的快捷方式。
uv check:让 uv 帮你跑类型检查
uv 0.11.18 之后可以用 uv check 跑类型检查(底层是 ty):
uv check它会自动管理类型检查器的安装和环境,不用单独装 ty 之类的。不算特别成熟,但日常用用够了。
uv.lock:理解锁文件
uv 的锁文件 uv.lock 跟 package-lock.json / Cargo.lock 是一个思路——记录了每个依赖的确切版本和哈希值。不管谁在什么机器上 uv sync,装出来的依赖完全一致。
几个值得注意的点:
uv sync --frozen:只看锁文件,不解析依赖。CI 里标配。uv sync --locked:检查锁文件是否和pyproject.toml一致,不一致就报错。也是 CI 常用。uv lock可以单独跑:你改了pyproject.toml的依赖约束,跑uv lock更新锁文件但不安装。- 锁文件格式换了:uv 0.11 开始默认用
pylock.toml格式,可读性比旧的uv.lock好很多。两个格式都支持,老的uv.lock还能用。
一个典型的工作流:
uv add httpx # 加依赖,自动更新锁文件 + 安装uv lock # 手动刷新锁文件(比如改了版本约束)uv sync --frozen # CI 或部署时按锁文件安装依赖组:组织不同类型的依赖
除了 dependencies 和 dev-dependencies,uv 支持自定义依赖组。用起来像这样:
[project]name = "my-project"dependencies = ["fastapi", "uvicorn"]
[dependency-groups]test = ["pytest", "httpx", "coverage"]lint = ["ruff"]docs = ["mkdocs", "mkdocs-material"]安装特定组:
uv sync --group test --group lint或者只装非开发依赖(部署环境):
uv sync --no-dev依赖组的好处是把”开发”这个概念拆得更细——测试、检查、文档各自一组,CI 里按需安装,不浪费构建时间。
配置文件 uv.toml
uv 支持全局和项目级配置文件,避免每次敲命令行参数。全局配置在 ~/.config/uv/uv.toml,项目级在 pyproject.toml 的 [tool.uv] 里。
一些我认为值得配的选项:
# 项目级 pyproject.toml[tool.uv]dev-dependencies = ["pytest", "ruff"]
# 全局 ~/.config/uv/uv.tomlpython-preference = "only-managed" # 只用 uv 自己管理的 Pythonconcurrent-downloads = 50 # 并发下载数,默认值通常够了uv 0.11 新增了几个有用的环境变量,CI 里可能用到:
| 变量 | 作用 |
|---|---|
UV_NO_INSTALL_PROJECT | 只装依赖不装项目本身 |
UV_NO_SYSTEM_CONFIG | 忽略系统级配置 |
UV_PYTHON_NO_REGISTRY | 不从网络找 Python 发行版 |
UV_NO_PROJECT | 完全不走项目模式 |
CI/CD 经验
总结几个我在 CI 里碰到的教训:
1. 始终用 --frozen
# GitHub Actions 示例- run: uv sync --frozen- run: uv run pytest这保证 CI 装的依赖和本地开发的完全一致。不加这个 flag,CI 可能在解析依赖时得到和本地不同的结果。
2. 缓存 uv 的环境
- uses: actions/cache@v4 with: path: ~/.cache/uv key: uv-${{ runner.os }}-${{ hashFiles('uv.lock') }}uv 的缓存目录存了下载的包和已解析的依赖信息,缓存后 uv sync 通常几秒就跑完。
3. 用 uv 安装 CI 工具
- run: uv tool run ruff check .- run: uv tool run ty .不需要在 CI 里 pip install ruff ty——uv tool run 自带缓存。
4. Python 版本用 .python-version 管
项目根目录放个 .python-version:
3.12CI 里:
- run: uv python install 3.12- run: uv sync --frozenuv 会严格按文件选 Python 版本,不会飘到系统默认的 Python 3.10 去。
常见问题
uv run 怎么知道用哪个 Python
uv 的 Python 发现顺序:命令行 --python > 项目 .python-version > 系统 python3。如果不确定当前用的是哪个:
uv run python -c "import sys; print(sys.executable)"旧项目怎么迁移到 uv
不需要一次性迁移。可以先只把 uv 当锁文件工具用:
cd old-projectuv lock # 从现有的 requirements.txt 或 pyproject.toml 生成锁文件uv sync # 用锁文件安装依赖迁移期间 pip 和 uv 可以共存——它们用同一个 venv 目录。
uv sync 很慢
第一次慢是正常的——uv 要下载所有包。之后的 uv sync 通常几秒完成。如果一直慢:
- 检查网络到 PyPI 的延迟(国内换个镜像源)
- 检查
uv.lock是不是太大(几百个包的话,解析本身就要时间) - 确认缓存目录没有被 CI 清掉
workspace 里包的导入路径对不上
workspace 子包默认以 editable 模式安装。如果 import my_core 找不到:
- 确认子包的
[project] name = "my-core"和目录名没关系——import 路径是包内src/下的 Python 模块名 - 确认根目录跑过
uv sync
uv tool install 的工具找不到
默认安装到 ~/.local/bin。确认这个路径在你的 PATH 里:
echo $PATH | tr ':' '\n' | grep localuv 0.11 之后可以用 uv tool dir --bin 看工具的安装路径。
总结
把这篇文章的内容串起来,一个挺顺手的工作流大概是:
- 单文件脚本用内联元数据 +
uv run,扔掉requirements.txt - 多包项目用 workspace,一个
uv sync搞定所有依赖 - 全局工具用
uv tool install,版本隔离不打架 - CI 里
uv sync --frozen+ 缓存~/.cache/uv,稳且快
uv 的设计思路和我用过的其他 Python 工具不太一样——它不做抽象层,不搞插件生态,就专注于把”解析依赖、下载包、管理环境”这件事做快做准。从 pip + venv + pip-tools 的组合迁移过来之后,我再也没回去过。