如果你和我一样,过去几年一直被 Python 项目的环境问题折腾,比如虚拟环境忘激活、依赖装错环境、换台机器就复现不了,那你看到 uv 这套命令时,多半会有一种“这次终于有人把常用操作收敛清楚了”的感觉。简单说,uv 是一套面向 Python 开发场景的命令行工具,它把依赖安装、虚拟环境、版本锁定、Python 解释器管理、甚至包构建发布都收进了同一条工作流里。我接下来要聊的,就是日常最常用的 uv 命令,每个命令解决什么问题、为什么这样设计、以及我迁移后踩过的坑。适合刚接触 uv 的新手,也适合正打算从传统 pip 流程切换过来的老手。
1. 换到 uv 之前,我先想清楚了一个问题:它省掉的到底是什么
1.1 传统工作流里,时间都耗在了“环境治理”上
以前做一个 Python 项目,标准流程大概是:先python -m venv .venv创建虚拟环境,再source .venv/bin/activate激活,接着pip install -r requirements.txt安装依赖。每次打开新终端,第一步永远是激活环境。如果同时维护三四个项目,很容易混淆当前到底在哪个环境里,有时候明明刚装过包,换个终端又找不到了,最后干脆把依赖装到系统 Python 里,一时省事,后面全是麻烦。
依赖一多,传统方式的问题会更明显。requirements.txt只记录直接依赖和某个时刻解析出来的版本,但传递依赖到底是谁引入的、版本是哪个,完全看不出来。某天一个间接依赖升级,可能引发一串连锁报错。这时候你只能靠pip freeze把整个环境导出来,但 freeze 出来的文件非常“实况”,本地环境是什么样,锁定的就是什么样,换个平台经常还是出问题。
1.2 uv 的设计思路:把项目目录当作唯一上下文
uv 的核心设计之一,是让命令基于项目目录自动工作,而不是依赖 shell 里的激活状态。它默认会在项目里创建.venv,当你执行uv run或uv sync时,它会自动定位到这个环境,不再要求你先执行 activate。所有依赖信息都集中在pyproject.toml和uv.lock里,项目目录就是唯一上下文。
这种收敛带来的好处很直接:你不用再问“我现在的环境是哪个”,你在哪个项目目录下执行命令,uv 就操作哪个项目的环境。再加上 uv 的底层是用 Rust 实现的,依赖解析和下载速度确实快,我用uv sync重建一个中等规模项目的环境,体感从以前的分钟级降到秒级。
1.3 高频 uv 命令总览
先给一张速查表,后面每个命令都会展开细讲:
| 命令 | 主要作用 | 典型用法 |
|---|---|---|
uv init | 初始化新项目结构 | uv init demo-project |
uv add | 添加依赖并同步环境 | uv add <包名> |
uv remove | 移除依赖 | uv remove <包名> |
uv sync | 按锁文件同步虚拟环境 | uv sync |
uv lock | 生成或更新锁定文件 | uv lock |
uv run | 在项目环境中执行命令 | uv run python main.py |
uv python | 管理 Python 解释器版本 | uv python install 3.12 |
uv tool | 隔离安装全局命令行工具 | uv tool install <工具名> |
uvx | 临时运行一个工具 | uvx <工具名> <参数> |
uv cache | 查看和清理全局缓存 | uv cache clean |
uv build | 构建 wheel 和 sdist | uv build |
uv publish | 发布包到包索引 | uv publish |
1.4 什么项目适合全面切到 uv
我的判断是:绝大多数 Python 项目都适合。个人脚本、库开发、Web 后端、数据分析项目,都能从这套命令里获益。唯一的例外可能是那种只有几十行、没有任何第三方依赖的临时脚本,这种直接用系统 Python 跑就行,没必要引入额外工具。
一旦项目出现“换台机器就装不上”“本地能跑 CI 挂”“不同人环境版本不一致”这类问题,就该考虑整体切换到 uv。它不是要多学一堆新概念,反而是把原来散落在多个工具里的概念统一到一起。
2. 新建项目三件套:uv init、uv add、uv remove 怎么配合
2.1 uv init 一次生成项目骨架
创建新项目最直接的方式就是uv init demo-project。执行后,目录里会生成pyproject.toml、.python-version,以及一个包目录或模块文件。pyproject.toml是整个项目的配置中心,几乎所有依赖声明、构建配置、工具配置都写在这个文件里。
.python-version则记录当前项目期望使用的 Python 版本,由uv python pin维护。这一下就把“项目元信息”和“环境元信息”都固定住了。我第一次用的时候有点意外,没想到 init 能直接生成这么完整的骨架,因为以前创建项目往往要手动补一堆配置文件。
2.2 uv add:添加依赖的正确姿势
给项目添加依赖,最标准的操作是:
uv add <包名>这条命令会做三件事:解析依赖的最新兼容版本、把依赖写入pyproject.toml的dependencies字段、更新uv.lock锁定文件,然后立即同步到当前项目的.venv里。也就是说,添加依赖后不需要再手动执行 install。
如果需要区分开发环境和生产环境的依赖,可以使用:
uv add --dev <包名>这样依赖会被写进开发依赖组,发布包时不会带上。我在实际项目里,通常会把测试、代码检查相关的包放在--dev,运行时核心依赖放在普通 dependencies 中。
2.3 uv remove:删除依赖也要同步三处
移除依赖对应的是uv remove <包名>。它同样会更新pyproject.toml、uv.lock和虚拟环境三处。这看起来简单,但恰恰是很多人容易忽略的点:如果直接手工删除pyproject.toml里的某个依赖,却没有更新锁文件和虚拟环境,那项目的声明状态和实际运行状态会不一致,后续排查问题会很痛苦。
2.4 手工编辑 pyproject.toml 后的正确操作
虽然 uv 命令已经覆盖了大多数场景,但偶尔你还是会想直接编辑pyproject.toml,比如调整版本范围、修改项目元数据。编辑完之后,正确的流程是:
uv lock uv sync第一步更新锁定文件,第二步让虚拟环境与锁文件保持一致。如果你确信只是改了个描述信息,不需要重新解析依赖,也可以直接uv sync,它会自动检测pyproject.toml是否变化。但为了 CI 里能稳定复现,最好养成“先 lock 后 sync”的习惯。
2.5 我踩过的小坑:手工加依赖导致版本冲突
有次我在pyproject.toml里手工加了一个包,没写版本范围,uv lock 时自然选了最新版。过了两天另一个依赖升级,两者之间出现不兼容,整个项目直接跑不起来。从那以后我尽量使用uv add而不是手工编辑,因为uv add会把当前解析结果和锁定文件一起更新,出问题的概率小很多。
3. uv run:为什么它能让我彻底忘掉 activate
3.1 activate 的痛点:环境状态是“记忆”出来的
过去每次打开终端,第一件事就是激活虚拟环境。问题在于,环境状态是存在 shell 会话里的,不是存在项目目录里的。你一旦开了新终端,就得重新激活;忘了激活,python指向的是系统解释器,依赖全部找不到。
还有个隐性成本:activate 修改的是环境变量,在多终端、多项目并行时非常容易出错。很多人到最后干脆全部装到全局环境,用“不管那么多了”的心态工作,后续代价更高。
3.2 uv run 的自动环境定位
uv run的设计思路完全不同。你只需要执行:
uv run python main.py它会先检查当前目录的项目状态:如果.venv不存在,就自动创建;如果依赖与锁定文件不一致,就自动同步;然后在这个环境中执行你给出的命令。所有这些判断,都基于项目目录,而不是 shell 状态。
除了跑 Python 脚本,uv run也可以执行任意命令:
uv run pytest uv run python -m http.server你甚至可以用它运行项目里安装的命令行工具,比如代码格式化工具或静态检查工具,不需要手动进入环境。
3.3 临时依赖注入
有时候我只想临时用某个工具跑一次,又不想把它写进项目依赖。这种场景用--with参数最合适:
uv run --with <临时包名> python -c "import demo; print(demo.__version__)"命令执行时,uv 会临时准备一个包含这些包的环境,跑完即弃,不污染项目配置。这个能力在做快速验证、临时对比不同库版本时非常有用。
3.4 几个重要参数
--no-sync:跳过自动同步,直接用当前已有的虚拟环境执行命令。--frozen:要求锁定文件存在且与pyproject.toml一致,不执行任何更新,这在 CI 中非常关键。--isolated:不使用项目虚拟环境,类似临时开一个干净环境。
这些参数解决的是同一个问题的不同侧面:什么时候允许改动环境,什么时候必须保持现状。
3.5 实际感受
我真正依赖上uv run,是在一次给同学演示项目启动步骤的时候。以前 README 里要写“先创建 venv,再激活,再安装依赖,最后运行”,现在只需要一句uv run python app.py。新手拿到项目,连虚拟环境是什么都不用理解,就能把项目跑起来。这种体验上的简化,比单纯的速度提升更重要。
4. uv lock 与 uv sync:从“能跑”到“可复现”
4.1 两种文件的分工
pyproject.toml里的依赖通常写的是版本范围,比如>=1.0,<2.0,意思是允许 1.x 系列内的升级。这个范围保证了灵活度,但也意味着今天装出来的环境和三个月后装出来的环境可能不同,因为没有锁定具体版本。
uv.lock则不同,它记录了解析后的精确版本,包括直接依赖和所有传递依赖,还有来源信息、校验信息。它回答的问题是:当前这一整套依赖,具体是哪一批版本。只要锁定文件存在,任何人都能重建出几乎一致的环境。
4.2 uv lock 什么时候需要手动执行
大多数情况下,uv add会自动更新锁文件,不需要手动执行。但遇到以下几种情况,我会手动跑:
- 手工修改了
pyproject.toml里的版本范围; - 想要升级某个特定包到最新兼容版本;
- 想要把全部依赖升级一遍。
升级单个包可以这样:
uv lock --upgrade-package <包名>全部升级则是:
uv lock --upgrade4.3 uv sync 的安装逻辑
uv sync会读取uv.lock,把里面记录的精确依赖安装到项目虚拟环境。它可以理解为“让当前环境与锁文件对齐”。当克隆一个新项目、切到新分支、或者同事更新了锁文件之后,我都直接执行一句话:
uv sync如果是在 CI 流水线里,我更推荐加上--frozen:
uv sync --frozen这样如果uv.lock与pyproject.toml不一致,命令会直接报错,而不是悄悄重新解析。CI 必须保持确定性,不能让一次自动更新悄悄改变测试环境。
4.4 一个容易出问题的点:不要手动改锁文件
uv.lock不是给人手工编辑的文件。它格式严格,内容由解析算法生成。有人试图“手动清理”里面的某些包,结果导致锁文件与pyproject.toml不一致,后面每次执行命令都会出现莫名其妙的同步错误。
如果锁文件已经损坏或混乱,最简单的恢复方式是把锁文件删掉,重新执行:
uv lock uv sync4.5 与旧方案 pip freeze 的核心差别
旧方案常用pip freeze > requirements.txt,本质是把某个环境里的“实况快照”导出来。问题在于,这个快照包含了当前环境的特殊状态,不一定代表项目的真实依赖关系。uv.lock 记录的是从pyproject.toml出发、经过完整解析得到的依赖图,带来源和校验信息,更接近其他语言生态里的标准 lockfile 思路。简单说,一个是“环境备份”,一个是“项目依赖图”,后者的可复现性更强。
5. uv python:解决“本机没有那个版本”的尴尬
5.1 安装指定版本的 Python
很多项目的报错,归根结底是 Python 版本不对。传统做法是去官网下载安装包,或者依赖系统自带的版本。uv 提供了更直接的方式:
uv python install 3.12它会自动下载对应版本的 Python 解释器,安装到 uv 自己的管理目录里,不干扰系统 Python。查看当前本机已有和可用的版本可以执行:
uv python list5.2 给项目固定开发版本
项目内希望统一 Python 版本时,使用:
uv python pin 3.11这会在项目根目录生成.python-version文件。之后所有uv run、uv sync都会优先使用这个版本。团队成员拿到项目,不用手动安装解释器,只要执行命令,就能在需要时自动下载并使用对应版本。
要注意一个区分:.python-version决定的是开发环境用哪个解释器;pyproject.toml里的requires-python决定的则是这个包本身支持哪些 Python 版本。二者需要配合,但并不是一回事。库项目尤其要把requires-python写清楚,否则用户可能在不支持的版本上安装。
5.3 多版本并行验证
当你需要验证代码在不同 Python 版本下的表现,可以临时指定:
uv run --python 3.10 python -c "print('hello')"这个能力在 CI 矩阵测试里也很方便:项目只需一个锁定文件,但可以在多个 Python 版本上分别运行测试。实际体验下来,比自己在系统里装一堆解释器再手动切换要省心得多。
5.4 uv python 子命令汇总
| 子命令 | 作用 |
|---|---|
uv python install | 安装指定版本 |
uv python list | 查看可用版本 |
uv python find | 查找某个版本的解释器路径 |
uv python pin | 给项目固定版本 |
uv python uninstall | 卸载不再需要的版本 |
5.5 一个小提醒
自动下载 Python 很省事,但如果你在公司内网、网络受限,可能需要提前把解释器包准备好,或者配置本地镜像源。我遇到过一次自动下载失败的情况,处理方式是在本机先uv python install 指定版本,等它进入缓存后再操作项目,后面就顺畅了。
6. uv tool 与 uvx:把命令行工具装进“独立包间”
6.1 全局安装工具的老毛病
有些工具是跨项目使用的,比如代码格式化、静态检查、项目管理类命令行工具。以前我习惯用pip install直接装到全局,后来发现两个问题:一是工具和项目依赖会混在一起,升级工具可能导致某个项目环境出问题;二是卸载工具时往往残留一堆依赖包,全局环境越来越脏。
6.2 uv tool install 的隔离安装
uv 提供了一个专门管理这类工具的子命令:
uv tool install <工具名>它会为这个工具单独创建一个环境,然后在用户目录里生成一个可执行命令入口。以后你在任意目录直接敲工具名,都能运行,但它不会污染任何项目环境。
常用的管理命令:
uv tool list # 查看已安装的工具 uv tool upgrade <工具名> # 升级某个工具 uv tool uninstall <工具名> # 卸载某个工具 uv tool dir # 查看工具环境的根目录6.3 uvx:连安装都省了
如果你只是偶尔用一次某个工具,不想长期维护它,可以用uvx:
uvx <工具名> <参数>uvx会在临时环境里解析并运行这个工具,用完即走。它有点像一个“免安装的运行器”,特别适合临时处理数据文件、格式转换这类低频任务。
6.4 工具环境与项目环境的边界
这里要强调一下:项目依赖永远用uv add和uv sync管理,全局命令行工具用uv tool管理。两者不要混用。我见过有人为了方便,把一个项目的依赖用uv tool install装了,结果命令入口全局可用了,但项目环境反而没装齐,构建时到处报 ModuleNotFoundError。
6.5 我的使用习惯
我现在所有跨项目工具都走uv tool,项目内依赖走uv add。这样每次重装系统或换电脑,只需要执行一条命令导入工具列表,就能快速恢复日常使用的命令行环境,项目内的依赖则各管各的,互不干扰。
7. uv cache 与离线场景:速度和安全感的来源
7.1 缓存机制:让重复安装变快
uv 会在全局维护一个缓存目录,下载过的包都会保存在里面。再次安装同一个包时,它可以直接从缓存复制或硬链接,而不是重新下载。这就是为什么同一个项目在多台机器、多个分支之间切换时,uv sync会感觉越来越快。
查看缓存目录位置:
uv cache dir7.2 常见缓存管理命令
uv cache clean:清空全部缓存,释放磁盘空间。uv cache prune:只清理不再需要的条目,保留可能还会用到的缓存。uv cache dir:查看当前缓存路径。
我一般不会随便执行clean,因为缓存是速度保障。只有在磁盘明显吃紧时,才会先prune再决定是否clean。
7.3 离线安装场景
uv 支持离线模式:
uv sync --offline uv add --offline <包名>只要所需依赖已经在缓存中,离线状态下也能完成安装或同步。这在内网部署、外出办公但网络不稳定的情况下非常实用。我第一次在无网络环境里成功重建项目环境时,最大的感受是:提前让缓存“吃饱”,比临时下载要可靠得多。
7.4 缓存位置与链接方式的配置
缓存目录可以通过环境变量修改,比如:
export UV_CACHE_DIR=/path/to/cache另外 UV_LINK_MODE 可以控制硬链接还是复制方式,不同文件系统下的表现不同。建议如果缓存目录和项目在同一块磁盘上,优先使用硬链接,安装速度快且不额外占用空间。具体效果和平台有关,实测为主。
7.5 缓存占空间的误区
缓存目录往往有几个 GB,看着吓人,但这是“用空间换速度”的典型场景。只要不过度膨胀,我倾向于不清理。尤其是喜欢在不同分支间切换的人,缓存命中一次就能省下升级依赖的大量时间,这笔账很划算。
8. uv build 与 uv publish:项目收尾的最后一公里
8.1 构建分发包
当项目需要发布给其他人安装时,要先构建出可分发的产物。执行:
uv build默认会在dist/目录下生成 wheel 和 sdist 两种格式。wheel 是常见的安装格式,sdist 是源码包。使用 uv build 前,需要在pyproject.toml里配置好[build-system],否则无法识别如何构建。
8.2 发布到包索引
构建完成后,发布命令非常短:
uv publish发布需要凭证,通常通过环境变量提供令牌,避免把密钥写进命令历史或配置文件。例如:
UV_PUBLISH_TOKEN=<你的令牌> uv publish如果发布到私有索引,还需要指定仓库地址,例如:
uv publish --publish-url <仓库地址>8.3 发布前的重要检查
发布后同名同版本的文件通常不允许覆盖,所以发布前我会习惯做一套检查:
- 确认
pyproject.toml里的版本号已经更新; - 确认 README、许可证等元数据文件存在且内容正确;
- 至少跑一遍测试命令;
- 检查
dist/里的构建产物文件列表是否齐全; - 实际在一个干净环境里安装一次构建产物,验证可用。
这些步骤每一条都很基础,但漏掉任意一条都可能导致发布出去的包无法使用,或者用户拿到的是缺失文件。
8.4 本地验证产物的方式
验证构建产物时,可以用uv run --with <本地wheel路径> python -c "import 包名"之类的临时环境方式,确保它不依赖当前项目的虚拟环境。这样可以模拟“用户拿到包之后从零安装”的真实场景,避免出现“本地能跑,别人装不上”的情况。
8.5 为什么要区分 build 和 publish
构建和发布分成两条命令,看起来多了一步,但实际上避免了“边构建边上传”的耦合。构建出的产物可以先检查、测试、签名,确认无误后再发布。如果构建产物有问题,你只需要重新构建,不需要担心已经上传的坏版本。
最后说一点个人体会。用了这么久,我最喜欢的其实不是某个单独命令,而是这套命令之间的连贯性:uv init出来一个项目,uv add添加依赖,uv lock和uv sync锁定环境,uv run执行代码,最后uv build和uv publish收尾。整个过程几乎没有“切工具”的断裂感。偶尔我还是会手工编辑pyproject.toml,但只要记住改完就跑一遍uv lock && uv sync,基本不会出乱子。如果你也想减少在环境管理上浪费的时间,从一个新项目开始尝试这套流程,是最没有风险的方式。