告别 pip + venv + pyenv 的混乱:现代 Python 包管理器 uv
写在前面
如果你用 Python 已经有一段时间,下面这些场景应该不陌生:
- 同时维护好几个项目,每个项目的 Python 版本都不一样,
pyenv、conda、系统python3互相打架; pip install一个大型框架(比如torch、transformers),风扇转得像要起飞,几分钟过去还卡在 "Collecting ...";- 换台机器部署,
requirements.txt装出来的环境和本地不一致,某个间接依赖升级后项目直接跑不起来; - 想起来还要
pipx装命令行工具、poetry做项目、twine发布包,一个 Python 生态愣是堆了七八个工具。
这些痛点不是你的错,是 Python 打包生态长期碎片化的结果。而 uv 正是为了解决这套混乱而生的。
这篇文章我会讲清楚三件事:uv 到底是什么、为什么值得你迁移过去、以及用一个从零到发布的完整案例带你把每个命令背后的设计意图都过一遍。
一、uv 是什么
uv 是由 Astral(也就是做出 Ruff 的那家公司)开发的 Python 包与项目管理工具,用 Rust 写成,以单个静态二进制文件分发,不依赖 Python 本身。
官方给它的定位是:
A single tool to replace pip, pip-tools, pipx, poetry, pyenv, twine, virtualenv, and more.
也就是说,它的野心不是"一个更快的 pip",而是要成为 Python 界的 Cargo------像 Rust 的 Cargo、Node.js 的 pnpm 那样,一个工具搞定从 Python 版本管理、虚拟环境、依赖解析、锁文件、命令行工具安装到打包发布的全流程。
它的几个核心特征:
- 极快:依赖解析和安装速度相比 pip 快 10--100 倍,这个量级不是营销话术,是 Rust 并行下载 + PubGrub 解析器 + 全局缓存共同带来的真实体验;
- 一体化 :
uv一个命令就涵盖了过去要pyenv + venv + pip + pip-tools + pipx + twine六个工具才能完成的事; - 标准兼容 :严格遵循 PEP 621(
pyproject.toml)、PEP 723(脚本内联依赖)等标准,生成的虚拟环境和锁文件其他工具也能读; - 跨平台锁文件 :一份
uv.lock能保证 macOS、Linux、Windows 拿到的是同一套依赖版本,真正意义上的可复现构建; - 磁盘友好 :全局缓存去重,十个项目都依赖
numpy,磁盘上只存一份。
二、为什么要使用 uv
"能用 pip 就行"这句话我自己也说过。真正让我切换到 uv 的,不是某个单点功能,而是它把我平时踩的坑系统性地堵上了。
1. 速度问题不再是个小问题
以前我觉得"多等几十秒没什么"。直到有一次在 CI 上跑测试,每次 pipeline 光装依赖就花两三分钟,一天下来所有人累计等上几小时。换成 uv 之后,带缓存的冷启动基本在秒级完成。对于频繁重建环境的场景(CI/CD、Docker 构建、本地反复试错),这个差异会直接影响工作节奏。
2. 锁文件真正做到了"锁"
pip freeze > requirements.txt 这个做法有个老问题:它冻结的是当前机器、当前平台 上解析出来的版本。同事在 Linux,你在 macOS,某些带本地扩展的包版本就可能不一样。uv 的 uv.lock 是跨平台的通用锁文件,一次解析,多平台可复现。
3. Python 版本管理也归它管
过去 pyenv 负责装 Python,venv 负责建虚拟环境,两者职责割裂。uv 直接 uv python install 3.13 就能下载并管理 Python 解释器本身,而且用的是 python-build-standalone 这种独立构建,不污染系统。
4. 命令行工具和项目依赖不再混淆
以前装个 black、ruff,该用 pip install 还是 pipx install?装到全局还是项目里?uv 用 uv tool install 明确区分了"我要的是一个能在命令行全局调用的工具",和项目依赖彻底隔离。
5. 迁移成本极低
uv 提供了 uv pip 子命令,接口和 pip 几乎完全一致。你可以先把 uv 当成一个更快的 pip 来用,等熟悉后再逐步迁移到 uv add / uv sync 这套更现代的工作流。
三、典型使用场景
下面这些场景,uv 都能给出比传统组合更省心的方案:
- 新建 Python 项目 :替代
poetry new/ 手动建venv+ 写setup.py; - 团队协作 :用
uv.lock保证每个成员环境一致,告别"在我机器上能跑"; - CI/CD 构建 :
uv sync --frozen基于锁文件快速还原环境,构建速度显著提升; - 数据科学 / 深度学习 :管理带 CUDA 的
torch、多 Python 版本切换试验; - 编写独立脚本 :PEP 723 内联依赖,一个
.py文件就自带依赖声明,uv run script.py直接跑; - 安装命令行工具 :
uv tool install代替pipx,速度更快; - 发布 PyPI 包 :
uv build+uv publish一条龙,不用再单独装build和twine。
四、从零开发一个项目:完整实战
光说不练没意思。下面用一个具体的小项目------一个抓取 GitHub 仓库 star 数的命令行工具------把 uv 的典型工作流走一遍。每一步我都会解释"做了什么"和"为什么这么做"。
步骤 0:安装 uv
macOS / Linux:
bash
curl -LsSf https://astral.sh/uv/install.sh | sh
Windows (PowerShell):
powershell
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
为什么这样装 :官方推荐的独立安装脚本会把 uv 二进制放到 ~/.local/bin/(旧版本在 ~/.cargo/bin/),这样 uv 完全独立于任何一个 Python 环境存在。这是它"不依赖 Python 也能管理 Python"的根本前提------如果你用 pip install uv 装它,就又陷入"先要有 Python 才能管 Python"的循环了。
验证安装:
bash
uv --version
步骤 1:初始化项目
bash
uv init github-stars
cd github-stars
执行后目录结构如下:
github-stars/
├── .gitignore
├── .python-version
├── README.md
├── main.py
└── pyproject.toml
每个文件的作用:
pyproject.toml:项目的单一信息源,遵循 PEP 621,声明项目名、版本、依赖、构建后端等所有元数据;.python-version:锁定项目使用的 Python 版本,uv 运行任何命令时都会读取它来选择解释器;.gitignore:自动生成,已经把.venv/、__pycache__/等都排除好了;main.py:入口示例文件。
为什么不是手动创建 :uv init 帮你一次性搭好符合现代 Python 打包规范的骨架。手动写 pyproject.toml 很容易漏掉关键字段(比如 build-system),导致后续打包出问题。
步骤 2:指定(或安装)Python 版本
bash
uv python install 3.13
uv python pin 3.13
做了什么 :第一条命令从 python-build-standalone 下载 CPython 3.13 到 uv 的管理目录(macOS 上在 ~/.local/share/uv/python/);第二条命令把版本号写入 .python-version。
为什么这么做:
- 不污染系统 Python:uv 管理的解释器和系统里的
python3完全隔离; - 团队一致性:
.python-version提交到 Git 后,同事uv sync时如果本机没有 3.13,uv 会自动下载,不需要他再去研究 pyenv; - 可审计:某一天某个库要求最低 Python 3.12,你在
.python-version里改一行就完事,不用重建整个环境。
步骤 3:添加项目依赖
我们这个工具要用到 httpx 发请求、rich 美化输出、click 做 CLI:
bash
uv add httpx rich click
你会看到输出里 uv 做了几件事:
- 自动创建
.venv/虚拟环境(如果还没有); - 解析依赖并下载到全局缓存;
- 把解析结果写入
uv.lock; - 把声明写入
pyproject.toml的[project].dependencies; - 把包硬链接(不是复制)到
.venv/里。
为什么是 uv add 而不是 pip install:
pip install只做安装,不更新pyproject.toml,你还得手动同步依赖声明;uv add是声明式 的:你告诉它"这个项目需要这些依赖",它负责解析、记录、安装、锁定一条龙。这个思路和 Cargo 的cargo add、npm 的npm install --save是一致的。
再加一个开发期才用的工具(比如测试框架):
bash
uv add --dev pytest
--dev 标志会把它放到 [dependency-groups].dev 下,发布时不会带上,但本地开发和 CI 能用到。
步骤 4:写代码
把 main.py 换成一个真正的小工具(简化版,便于说明流程):
python
import click
import httpx
from rich.console import Console
from rich.table import Table
console = Console()
@click.command()
@click.argument("repos", nargs=-1, required=True)
def main(repos: tuple[str, ...]) -> None:
"""查询一个或多个 GitHub 仓库的 star 数,格式:owner/repo"""
table = Table(title="GitHub Stars")
table.add_column("Repository", style="cyan")
table.add_column("Stars", justify="right", style="green")
with httpx.Client(timeout=10.0) as client:
for repo in repos:
resp = client.get(f"https://api.github.com/repos/{repo}")
if resp.status_code == 200:
table.add_row(repo, f"{resp.json()['stargazers_count']:,}")
else:
table.add_row(repo, "[red]N/A[/red]")
console.print(table)
if __name__ == "__main__":
main()
步骤 5:运行代码
bash
uv run main.py astral-sh/uv astral-sh/ruff
uv run 做了什么:
- 检查
.venv/是否存在且和uv.lock一致,如果不一致会自动同步; - 在虚拟环境内执行命令,但不需要你手动
source .venv/bin/activate; - 结束后环境依然在那里,下次调用无需重启。
为什么这是个大进步 :传统流程是 source .venv/bin/activate && python main.py,切到别的项目前还要 deactivate。uv run 把激活/退出完全隐藏了,你只需要关心"我要跑什么",不用关心"当前 shell 处于什么状态"。CI 脚本也因此变得更短、更不容易出错。
步骤 6:查看依赖树
bash
uv tree
会打印出完整的依赖树,包括间接依赖。调试"为什么装了这个奇怪的包"时非常有用。
步骤 7:锁定与同步
把项目推到 Git 后,同事 clone 下来:
bash
git clone <repo>
cd github-stars
uv sync
uv sync 做的事:
- 读取
.python-version,必要时自动下载对应 Python 版本; - 读取
uv.lock,按锁文件里精确到 hash 的版本还原环境; - 创建
.venv/并安装所有依赖。
为什么要提交 uv.lock :它是可复现构建的保证。pyproject.toml 里写的是"我要 httpx >= 0.27"这种范围约束,而 uv.lock 记录的是"我们实际用的是 httpx 0.28.1,它依赖 anyio 4.4.0..."这种精确快照。提交锁文件,CI 和同事的环境就和你严格一致。
在 CI 里建议用更严格的 --frozen:
bash
uv sync --frozen
这会拒绝任何偏离锁文件的情况,如果 pyproject.toml 和 uv.lock 不一致会直接报错,防止"改了依赖但忘了提交锁文件"的事故。
步骤 8:升级依赖
bash
uv lock --upgrade-package httpx
只升级指定的包;要全量升级则用 uv lock --upgrade。升级后记得 uv sync 让本地环境跟上。
为什么不用 pip install -U :pip install -U 会直接装到环境里,但不会告诉你"这次升级同时带起来了哪些间接依赖变更"。uv 的做法是先更新锁文件再同步,升级过程完全透明、可审查 ,你可以 git diff uv.lock 看到所有变化。
步骤 9:构建与发布
如果要把这个工具发到 PyPI:
bash
uv build
uv publish
uv build生成dist/下的 wheel 和 sdist;uv publish上传到 PyPI(需要配置 token)。
为什么这很爽 :以前要 pip install build twine,然后 python -m build、twine upload dist/*。现在一个 uv 搞定,不需要额外装工具。
步骤 10(额外):单文件脚本模式
如果你的需求小到"就是一个脚本",连项目目录都懒得建,可以用 PEP 723 内联依赖:
新建 quick.py:
python
# /// script
# requires-python = ">=3.12"
# dependencies = ["httpx", "rich"]
# ///
import httpx
from rich import print
print(httpx.get("https://api.github.com/repos/astral-sh/uv").json()["stargazers_count"])
执行:
bash
uv run quick.py
uv 会读取文件头里的元数据,临时创建一个隔离环境把依赖装进去再执行。脚本跑完环境进入缓存,下次再跑直接复用。
这解决了什么痛点 :以前分享一个 Python 小脚本给别人,还要附一句"先 pip install 这几个包"。现在脚本自己就包含依赖声明,对方只要有 uv,uv run script.py 就够了。
五、常用命令速查
| 目的 | 命令 |
|---|---|
| 初始化项目 | uv init <name> |
| 安装 Python 版本 | uv python install 3.13 |
| 固定项目 Python 版本 | uv python pin 3.13 |
| 添加依赖 | uv add <pkg> |
| 添加开发依赖 | uv add --dev <pkg> |
| 移除依赖 | uv remove <pkg> |
| 运行命令 | uv run <cmd> |
| 同步环境 | uv sync |
| CI 严格同步 | uv sync --frozen |
| 查看依赖树 | uv tree |
| 升级锁文件 | uv lock --upgrade |
| 全局安装 CLI 工具 | uv tool install ruff |
| 临时运行 CLI 工具 | uvx ruff check |
| 构建发布物 | uv build |
| 发布到 PyPI | uv publish |
| pip 兼容接口 | uv pip install/freeze/... |
六、迁移建议与小结
对已有项目,我的迁移路径推荐是:
- 第一阶段 :先用
uv pip替代pip,零成本感受速度提升; - 第二阶段 :用
uv init在新分支上重建项目,把requirements.txt里的依赖用uv add重新声明,生成pyproject.toml和uv.lock; - 第三阶段 :CI 改成
uv sync --frozen,团队统一切换。
uv 不是"又一个包管理器",它更像是 Python 生态打包这个问题被重新严肃思考之后的一个答案。当然它也不是完美的------比如对某些极端定制的 index 配置、复杂 monorepo 场景仍在演进,早期项目遇到问题建议直接去翻 GitHub issue 区,社区响应速度很快。
但就日常开发体验而言,它值得你花一个下午迁移过去。速度只是表象,一个工具搞定所有事带来的心智负担减轻才是真正的价值。
参考链接
- 官方文档:https://docs.astral.sh/uv/
- GitHub 仓库:https://github.com/astral-sh/uv
- Astral 博客(uv 发布文):https://astral.sh/blog/uv
- PEP 621(项目元数据):https://peps.python.org/pep-0621/
- PEP 723(脚本内联元数据):https://peps.python.org/pep-0723/