uv入门使用记录

为什么会开始用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
2
brew install uv      # macOS
scoop install uv # Windows

具体的安装方式可以参考官方安装文档 Installing uv。

装完之后:

1
uv --version

只有一个二进制文件,不到 30MB,不依赖任何 Python 运行时。

常用工作流

uv 的项目概念围绕 pyproject.toml 和 uv.lock 展开。下面以一个文本分类器项目 text-classifier 为例,展示完整的 uv 工作流。

创建项目

1
2
uv init text-classifier
cd text-classifier

生成的结构:

1
2
3
4
5
6
text-classifier/
├── .python-version
├── pyproject.toml
├── README.md
├── uv.lock
└── hello.py

pyproject.toml 遵循标准的 PEP 621 格式,不是 uv 专有的——这意味着即使以后不用 uv 了,项目定义依然是可移植的。关于项目初始化的更多选项(比如指定 Python 版本、创建应用 vs 库项目),见官方项目文档。

管理依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 添加依赖(不指定版本 — uv 自动选最新兼容版本)
uv add numpy pandas scikit-learn

# 精确控制版本号
uv add "numpy>=1.24,<2.0" # 版本范围
uv add "scikit-learn==1.5.0" # 锁定精确版本
uv add "torch~=2.3.0" # 兼容版本(>=2.3.0, ==2.3.*)
uv add "pandas>=2.0" # 最低版本

# 开发依赖
uv add --dev pytest black ruff

# 移除依赖
uv remove numpy

版本号语法遵循 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
2
3
4
5
6
7
8
9
[project]
name = "text-classifier"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"numpy>=1.24,<2.0",
"pandas>=2.0",
"scikit-learn==1.5.0",
]

uv add --dev 加的开发依赖写入 [dependency-groups].dev,和运行时依赖是分开的。加上 pytest / black / ruff 之后,pyproject.toml 的两个依赖区:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[project]
name = "text-classifier"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"numpy>=1.24,<2.0",
"pandas>=2.0",
"scikit-learn==1.5.0",
]

[dependency-groups]
dev = [
"pytest>=8.0",
"black>=24.0",
"ruff>=0.5",
]

两者的区别在于什么场景下被安装:

1
2
uv sync           # 装 dependencies + dev dependencies(默认,本地开发用)
uv sync --no-dev # 只装 dependencies,跳过 pytest/black/ruff(CI 用)

CI 里用 --no-dev 可以省掉不必要的时间和空间,本地直接 uv sync 全装就好。

uv.lock 则记录了解析后的精确版本(包括所有传递依赖的 hash)。如果想看某个包当前锁定的版本:

1
2
uv tree                    # 查看完整依赖树
uv tree --depth 1 # 只看直接依赖

升级某个包到最新兼容版本:

1
2
uv lock --upgrade-package numpy   # 只升级 numpy
uv lock --upgrade # 升级全部

同步环境

1
uv sync

读取 uv.lock,把环境里的包精确同步到锁定版本。别人拉了代码之后,跑一次 uv sync 就能拿到完全一致的环境。

运行命令

1
2
3
uv run python train.py         # 运行训练脚本
uv run python -c "import sklearn; print(sklearn.__version__)"
uv run pytest # 跑测试

不需要先激活 venv。uv run 自动使用项目目录下的 .venv,写 Makefile 或者 CI 脚本的时候很方便。

Python 版本管理

uv 内建了类似 pyenv 的 Python 版本管理(详见 Python versions):

1
2
3
uv python install 3.12    # 安装指定 Python 版本
uv python list # 查看可安装的版本
uv python pin 3.12 # 为当前项目锁定版本

不需要提前手动装 Python,uv 会自动下载。.python-version 文件和 pyproject.toml 里的 requires-python 会一起决定用哪个 Python。

工具管理

有些 Python CLI 工具(比如 ruff、pre-commit)不需要捆绑到项目依赖里。uv 提供了类似 pipx 的工具管理模式(Using tools):

1
2
3
uv tool install ruff       # 全局安装,自动隔离环境
uv tool list # 查看已装工具
uvx ruff check . # 不安装直接运行(uvx = uv tool run)

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
2
3
4
5
# 导出 conda 环境中的 pip 依赖
conda list --export | grep -v "^#" | cut -d " " -f 1 > requirements.txt

# 或者只导出 pip 安装的包
pip freeze > requirements.txt

也可以用 conda 导出 environment.yml:

1
conda env export > environment.yml

2. 初始化uv项目并导入依赖

1
2
3
4
uv init                    # 如果还没有 pyproject.toml
uv add -r requirements.txt # 导入 pip 格式的依赖列表
uv lock # 生成 uv.lock
uv sync # 同步环境

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
2
3
my-project/
├── environment.yml # conda 环境定义
└── src/...

迁移后:

1
2
3
4
5
6
7
8
9
10
# 用 conda 装系统级依赖
conda create -n sys-deps cuda-toolkit cmake gcc -y

# 用 uv 管理 Python 项目
cd my-project
uv init
uv add torch numpy pandas
uv add --dev pytest
uv sync
uv run python src/main.py

总结

uv 不是什么革命性的东西——它做的事情和 pip / conda 做的是一样的:装包、管环境、锁版本。但它把这些事做得更快、更统一。

对我个人来说,最大的收益是两点:(1) 不用再等依赖解析了——uv add 基本瞬时完成;(2) uv.lock 让项目的依赖可复现性有了一个清晰的机制,不像 conda 的 environment.yml 那样经常出现"你版本不对"的问题。

当然 conda 仍然是装非 Python 依赖的最方便工具,这一点 uv 目前替代不了。但如果你大部分时间都在写纯 Python 项目,从 conda 切到 uv 的体验提升是实打实存在的。