为什么会开始用uv
最近参与的一些开源项目开始用 uv 作为包管理器,pyproject.toml 旁边都有一个 uv.lock。之前一直是用 conda 管理 Python 环境的,不得不说 conda 在装一些非 Python 的二进制依赖(比如 cudatoolkit)时确实好用,但每次 conda install 都要等依赖解析等到怀疑人生,而且一个环境动不动就几个 G,还会有一些奇怪的版本冲突问题。
于是花了一个下午翻了翻 uv 的文档,把它常用的工作流捋了一遍,记录在这里。后面也会和 conda 做个对比,方便之后决定什么场景用什么工具。
本文基于 uv 的官方文档 uv docs 和 conda 的官方文档 conda docs 整理,写这篇文章时的 uv 版本是 0.8.x。部分内容参考了 Astral 团队发布的介绍博客 uv: The Next-Generation Python Package Manager。
uv是什么
uv 是 Astral 团队(也是 ruff 的开发者)用 Rust 写的 Python 环境和包管理器。核心卖点就一个:快。依赖解析和下载都比 pip 快一个数量级。
它的目标是统一 Python 工具链里的各种工具——pip、venv、pyenv、pipx、poetry 等等——全部合并到一个二进制文件里。按照官方文档的说法,uv 既可以作为单个工具替换 pip 的工作流(uv pip install),也可以作为完整的项目管理工具(uv init + uv add + uv sync)来用。
安装
一行命令(macOS / Linux):
1 | curl -LsSf https://astral.sh/uv/install.sh | sh |
Windows:
1 | powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" |
也可以用包管理器:
1 | brew install uv # macOS |
具体的安装方式可以参考官方安装文档 Installing uv。
装完之后:
1 | uv --version |
只有一个二进制文件,不到 30MB,不依赖任何 Python 运行时。
常用工作流
uv 的项目概念围绕 pyproject.toml 和 uv.lock 展开。下面以一个文本分类器项目 text-classifier 为例,展示完整的 uv 工作流。
创建项目
1 | uv init text-classifier |
生成的结构:
1 | text-classifier/ |
pyproject.toml 遵循标准的 PEP 621 格式,不是 uv 专有的——这意味着即使以后不用 uv 了,项目定义依然是可移植的。关于项目初始化的更多选项(比如指定 Python 版本、创建应用 vs 库项目),见官方项目文档。
管理依赖
1 | # 添加依赖(不指定版本 — uv 自动选最新兼容版本) |
版本号语法遵循 PEP 508 规范。常用的版本操作符:
| 操作符 | 含义 | 示例 |
|---|---|---|
>= |
不低于 | "numpy>=1.24" — 1.24 及以上 |
<= |
不高于 | "numpy<=2.0" — 2.0 及以下 |
== |
精确版本 | "torch==2.3.0" — 只要 2.3.0 |
~= |
兼容版本 | "pandas~=2.0" — >=2.0, <2.1 |
!= |
排除版本 | "numpy!=1.24.0" — 跳过有 bug 的版本 |
>, < |
严格大小 | "numpy<2.0" — 1.x 系列 |
可以组合使用:uv add "numpy>=1.24,<2.0,!=1.24.3"。
每次 uv add 后,uv 自动更新 pyproject.toml 和 uv.lock。给 text-classifier 加上依赖后,pyproject.toml 的 dependencies 部分变成:
1 | [project] |
uv add --dev 加的开发依赖写入 [dependency-groups].dev,和运行时依赖是分开的。加上 pytest / black / ruff 之后,pyproject.toml 的两个依赖区:
1 | [project] |
两者的区别在于什么场景下被安装:
1 | uv sync # 装 dependencies + dev dependencies(默认,本地开发用) |
CI 里用 --no-dev 可以省掉不必要的时间和空间,本地直接 uv sync 全装就好。
uv.lock 则记录了解析后的精确版本(包括所有传递依赖的 hash)。如果想看某个包当前锁定的版本:
1 | uv tree # 查看完整依赖树 |
升级某个包到最新兼容版本:
1 | uv lock --upgrade-package numpy # 只升级 numpy |
同步环境
1 | uv sync |
读取 uv.lock,把环境里的包精确同步到锁定版本。别人拉了代码之后,跑一次 uv sync 就能拿到完全一致的环境。
运行命令
1 | uv run python train.py # 运行训练脚本 |
不需要先激活 venv。uv run 自动使用项目目录下的 .venv,写 Makefile 或者 CI 脚本的时候很方便。
Python 版本管理
uv 内建了类似 pyenv 的 Python 版本管理(详见 Python versions):
1 | uv python install 3.12 # 安装指定 Python 版本 |
不需要提前手动装 Python,uv 会自动下载。.python-version 文件和 pyproject.toml 里的 requires-python 会一起决定用哪个 Python。
工具管理
有些 Python CLI 工具(比如 ruff、pre-commit)不需要捆绑到项目依赖里。uv 提供了类似 pipx 的工具管理模式(Using tools):
1 | uv tool install ruff # 全局安装,自动隔离环境 |
uv和conda的对比
之前一直用 conda,切换到 uv 之后最大的感受是快了很多,但也少了一些东西。下面从几个维度做个对比。
安装速度与依赖解析
这是 uv 最大的优势。Rust 实现的解析器加上全局缓存和并行下载,日常 uv add 和 uv sync 基本是秒级完成。conda 的依赖解析(特别是用默认 channel 的时候)慢是出了名的——加一个包有时要解几分钟。
uv 官方宣称比 pip 快 10-100 倍,我在自己机器上的体感是:一个中等规模的深度学习项目,conda 从 environment.yml 重建环境大约需要 3-5 分钟,uv 从 uv.lock 重建不到 30 秒。
依赖管理方式
| uv | conda | |
|---|---|---|
| 依赖声明 | pyproject.toml(PEP 621标准) |
environment.yml 或 conda.ini |
| 锁文件 | uv.lock(跨平台精确锁定) |
无内置锁文件机制 |
| 包源 | PyPI(Python包) | conda-forge / defaults(Python + 非Python包) |
| 依赖解析器 | Rust实现,快 | 基于SAT solver,慢 |
uv 的 uv.lock 是一个跨平台的精确锁定文件,这一点 conda 没有内置的等价物。conda 的 environment.yml 可以指定版本范围,但要精确复现通常需要额外导出 conda list --export。
环境隔离
conda 的环境是全局的,所有环境在一个统一目录下管理(conda info --envs 查看)。好处是切换方便,坏处是不同项目的环境容易混。
uv 的环境默认建在项目目录下的 .venv 里,天然和项目绑定。这个设计的思路和 npm 的 node_modules 类似——项目和环境是一体的。我更喜欢这种方式,删项目的时候环境也跟着删了,不会留下一堆孤儿环境。
非Python依赖
这是 conda 真正的护城河。conda 可以管理任何二进制包——Python、R、C 库、CUDA toolkit 等等。比如装个 PyTorch + CUDA:
1 | conda install pytorch torchvision torchaudio pytorch-cuda=12.1 -c pytorch -c nvidia |
uv 只能管 Python 包。如果需要 ffmpeg、cmake、cuda-toolkit 这类系统级依赖,要么用系统包管理器(apt/brew),要么继续搭配 conda 使用。这是 uv 目前最大的局限。
Python版本管理
conda 在创建环境时可以直接指定 Python 版本:
1 | conda create -n myenv python=3.11 |
uv 通过 uv python install + uv python pin 实现类似功能。两者用起来差别不大,uv 会在首次使用时自动下载对应 Python 版本。
使用场景建议
| 场景 | 推荐工具 | 原因 |
|---|---|---|
| 纯 Python 项目(Web、ML 训练脚本等) | uv | 快,轻量,uv.lock 保证可复现 |
| 需要 CUDA / 非 Python 二进制依赖 | conda(或 conda + uv 混用) | conda 的二进制包生态无可替代 |
| 开源项目 / 团队协作 | uv | pyproject.toml + uv.lock 跨平台,CI 友好 |
| 数据科学 / 快速实验 | conda | conda-forge 生态成熟,一行装全家桶 |
| 已有 conda 项目长期维护 | 保持不变 / 渐进迁移 | 没坏就不修,除非有痛点在推动 |
我自己目前的策略是:新项目优先用 uv,涉及 CUDA 或者需要 conda-forge 上才有的包时继续用 conda。有时候也混用——用 conda 装系统级依赖和 CUDA,用 uv 管理 Python 包。
从conda迁移到uv
如果你已经有一个 conda 管理的项目,迁移到 uv 大致分三步。详细的迁移指南可以参考 uv 官方的 Migration Guide。
1. 导出当前环境依赖
1 | # 导出 conda 环境中的 pip 依赖 |
也可以用 conda 导出 environment.yml:
1 | conda env export > environment.yml |
2. 初始化uv项目并导入依赖
1 | uv init # 如果还没有 pyproject.toml |
3. 处理非 Python 依赖
conda 里装的非 Python 包(cuda-toolkit、gcc、cmake 等)uv 管不了。有几种处理方式:
- 继续用 conda 装系统依赖:
conda install cuda-toolkit cmake,然后 Python 的包全部交给 uv。这种方式其实不冲突——只要确保 conda 环境里的 Python 和 uv 用的是同一个版本。 - 用系统包管理器:macOS 上
brew install cmake,Ubuntu 上apt install build-essential。 - 用 Docker:系统依赖打进镜像,Python 依赖在镜像里用 uv 装。
我个人倾向于第三种(Docker),一来环境最干净,二来 CI 里也好复现。但如果只是本地开发,混用 conda + uv 也没什么问题——反正 conda 就是一堆二进制文件的搬运工,和 pip/uv 并不矛盾。
完整迁移示例
假设原项目结构:
1 | my-project/ |
迁移后:
1 | # 用 conda 装系统级依赖 |
总结
uv 不是什么革命性的东西——它做的事情和 pip / conda 做的是一样的:装包、管环境、锁版本。但它把这些事做得更快、更统一。
对我个人来说,最大的收益是两点:(1) 不用再等依赖解析了——uv add 基本瞬时完成;(2) uv.lock 让项目的依赖可复现性有了一个清晰的机制,不像 conda 的 environment.yml 那样经常出现"你版本不对"的问题。
当然 conda 仍然是装非 Python 依赖的最方便工具,这一点 uv 目前替代不了。但如果你大部分时间都在写纯 Python 项目,从 conda 切到 uv 的体验提升是实打实存在的。