拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

UV迁移指南:告别Python依赖泥潭,从requirements到pyproject

UV迁移指南:告别Python依赖泥潭,从requirements到pyproject

“No module named xxxx”——新同事第一次拉代码,从项目目录往上找 VC 环境,三个 Python 版本谁也说不清哪个要干活。如果你也在旧项目的依赖泥潭里挣扎,这篇就直接说说 UV,以及最痛苦的旧项目接入要怎么一步步来,尽量少走我当年踩过的弯路。

UV 是近两年 Python 生态里最值得关注的环境管理工具,用 Rust 重写,核心解决两件事:一是环境创建和依赖安装快到离谱,二是把 pyproject.toml 变成唯一可信的项目声明,配合 uv.lock 锁住整棵依赖树。它既能当 pip 的高级替代品,也能像 poetry 那样做完整项目管理,还能接管 Python 解释器版本本身。适合谁?被 conda 搞懵的、和 requirements.txt 打了一辈子仗的、项目里混着 setup.py 和一堆 C 扩展的,都值得读完这篇再动手。

1. 为什么是 UV:从 pip/poetry 手里抢过环境管理权

1.1 快,但不只是快

老 Python 开发都懂,pip install -r requirements.txt第一次跑,经常是泡杯咖啡回来还没结束。UV 让我第一次感受到"环境是秒建的”。核心原因有几个:依赖解析用的 Rust 写的 resolver,比 pip 自带的回溯算法快一个大数量级;下载是并行的,而且走全局缓存,同一个包在不同项目里装过,第二次直接本地拷贝;创建虚拟环境时直接复制标准库文件而不是逐个路径算。

我自己测过同一个 FastAPI 项目,requirements 大约四十个包,conda 创建加安装大概 3 分钟,pip 冷缓存差不多 2 分钟,UV 冷缓存 40 秒,热缓存不到 10 秒。这不是高配机器上的理论值,就是普通 Windows 笔记本的实测。

但 UV 真正有价值的不是"装得快",而是它把"环境应该长什么样"这件事标准化了。以前每个项目都有一套心照不宣的约定:依赖放 requirements.txt,dev 依赖放 requirements-dev.txt,版本有时候钉死有时候不钉,解释器版本靠 README 里一句话。UV 出现之后,这些东西被统一收进 pyproject.toml 和 uv.lock,人眼和机器都能看懂。

1.2 一个文件管到底

用 pip 时代,项目元数据是 setup.py / setup.cfg,运行时依赖是 requirements.txt,dev 依赖再来一个文件,lint 和 test 工具自己的配置又散落在 .flake8、pytest.ini、.pre-commit-config.yaml。UV 的项目模式把这些收敛到 pyproject.toml,至少依赖和项目信息不再散落一地。

注意,这不意味着你需要把所有工具配置都塞进去。我的建议是:依赖声明、项目名称版本、Python 版本约束这些必须进 pyproject.toml,工具配置保持原样不动。迁移初期少动别的,只动依赖相关,出问题好排查。

1.3 和 pip / conda / poetry 的直观对比

能力pip + venvcondapoetryUV
依赖解析弱,容易挂中等强强
锁定文件无(requirements 不算)无poetry.lockuv.lock
Python 版本管理不支持支持不支持(需 pyenv)支持
安装速度慢较慢中等快
缓存复用弱中等中等强
pip 风格兼容天然有限差好
项目模式无无有有

poetry 用户会问:既然有 poetry 了,为什么还要 UV?我的体会是,poetry 用 pip 作为后端解析,遇到复杂依赖树,性能还是不够,而且 poetry 的依赖分组和 activate 方式跟传统团队习惯差异大。UV 的兼容层做得更好——老项目的 pip 命令不用改,新项目可以直接走uv add / uv sync的现代流程,平滑得多。

2. UV 环境管理基础:安装、虚拟环境与 Python 版本切换

2.1 安装 UV 的三种方式

官方推荐一键脚本:

# macOS / Linux curl -LsSf https://astral.sh/uv/install.sh | sh # Windows PowerShell irm https://astral.sh/uv/install.ps1 | iex

但实际团队里,我见过更接地气的两种:

# 用 pip 装(适合网络环境已经通的公司镜像) pip install uv # Windows 上用 winget winget install --id=astral-sh.uv

建议装完立刻跑一次uv --version确认。之后升级直接uv self update,不需要重新走安装脚本。

安装后,二进制默认放到用户目录(Windows 是C:\Users\Administrator\AppData\Local\uv,macOS 是~/.local/bin),缓存目录在同级,可以用UV_CACHE_DIR环境变量改到别的盘,避免 C 盘爆掉。这个后面讲坑的时候还会再提。

2.2 两种模式:pip 兼容模式 vs 项目模式

UV 最妙的地方在于,它同时提供两种完全不同的工作方式,互不冲突:

pip 兼容模式:uv pip install、uv pip freeze、uv pip list,语法几乎和 pip 一模一样,适合你只想换个快一点的 pip,不想改变现有工作流。

uv pip install -r requirements.txt

项目模式:以pyproject.toml和uv.lock为核心,是 UV 真正体现出差异化价值的用法。

uv init uv add requests uv sync uv run python main.py

这个模式下不需要手动激活虚拟环境,uv run会在当前项目里自动找到.venv并执行命令,省掉了source activate这一步。

要理解两者区别,可以把 pip 兼容模式想象成"临时搭个脚手架干活",项目模式是"把工地永久规范化"。对新上手的朋友,我建议直接学项目模式,日常开发体验好很多。

2.3 切换环境和 Python 版本

热搜里那个"uv 切换环境",其实包含三个层面,很多人一开始会被绕晕:

第一层:Python 解释器版本切换。UV 自带 Python 管理能力,不需要 pyenv 了:

uv python install 3.12 uv python list

在项目根目录执行:

uv python pin 3.11

会在项目下生成一个.python-version文件,之后uv sync会自动找对应版本的 Python。团队协作时把这个文件提交进 git,全员统一解释器版本。

第二层:项目虚拟环境切换。每个项目有自己的.venv,uv venv创建。你从一个项目工作区切到另一个,不用离开终端,uv run自动识别当前目录的 pyproject.toml 和.venv。以前是"手动激活 A,再激活 B",现在是"在哪个目录就执行哪个环境"。

uv venv # 创建 .venv uv venv --python 3.12 # 指定版本创建

第三层:临时隔离环境和工具。偶尔想跑个脚本但不想污染项目依赖:

uv run --with requests python script.py uv run --with "numpy>=2.0" python -c "import numpy"

这会临时构造一个隔离环境,脚本跑完就丢,对 macOS 上经常写一次性数据分析脚本的人来说简直是救星。

2.4 缓存目录和常用维护命令

UV 的全局缓存是提速的关键,但时间久了也挺占空间。Windows 上默认在C:\Users\Administrator\AppData\Local\uv\cache,macOS 在~/Library/Caches/uv。查看和清理:

uv cache dir uv cache clean

uv cache clean会清空全部缓存,代价是下次创建环境重新下载。我更推荐先uv cache prune,只清理没用的旧版本,保留常用包。

常用命令速览:

uv sync # 按 pyproject.toml 同步依赖并创建/更新 .venv uv add requests # 添加依赖并更新 uv.lock uv remove requests # 移除依赖并更新 uv.lock uv lock # 只生成 uv.lock,不创建环境 uv run python -c "print('ok')" # 在项目环境里跑命令

3. 旧项目接入 UV 的完整迁移流程

3.1 先给旧项目做个体检

接入 UV 之前,先搞清楚旧项目现在靠什么活着。我归纳下来,基本逃不过这四种情况:

  1. 只有requirements.txt和requirements-dev.txt,没有 pyproject.toml
  2. 有setup.py,可能是传统库项目
  3. 有 pyproject.toml,但用的是 setuptools 后端
  4. 更惨一点的,pipenv 的 Pipfile

先别急着删任何文件。迁移的一个铁律是:不要破坏现有可运行状态,新老方式共存一段时间,确认无问题再清理。我把 pyproject.toml 之外的所有声明文件备份进legacy/目录,而不是直接扔 trash,这样随时能回退。

看一下requirements.txt里的版本约束风格也重要。老项目常见两种:完全钉死requests==2.31.0,或者宽松requests>=2.0。后者在 UV 的严格 resolver 下,可能引发解析问题,后面第 4.1 节专门讲。

3.2 生成 pyproject.toml

在项目根目录执行:

uv init --bare --python=3.11

--bare是关键,它只生成一个最简 pyproject.toml,不会帮你创建示例代码和 README:

[project] name = "my-legacy-project" version = "0.1.0" description = "" readme = "" requires-python = ">=3.11" dependencies = []

稍微解释一下字段:requires-python是项目对解释器版本的硬性要求,会被 UV 的 resolver 拿来筛选包版本;dependencies是我们下一节要填充的运行时依赖列表。

如果旧项目是纯应用而不是库,我还建议加一段:

[tool.uv] package = false

这让 UV 不把当前项目当包构建,避免咬着setup.py不放,减少很多麻烦。对 Web 服务、脚本项目这个配置非常实用。

3.3 把 requirements 批量转成正式依赖

这是整个迁移最核心的一步。我们想把 requirements.txt 里那些依赖,变成 pyproject.toml 里的dependencies,而不是继续用uv pip install -r绕过项目模式。

最简单的方式一步到位:

uv add -r requirements.txt

如果 UV 版本较旧不支持-r,或者某些行(比如带-e的可编辑安装、还带注释的行)解析失败,就用兜底方案。我的习惯是先把 requirements 文件里空行、注释、--index-url这种辅助行全部过滤掉,只留包行:

uv add $(grep -vE '^#|^$|^-e|^--' requirements.txt)

dev 依赖同理,但用参数区分分组:

uv add --dev -r requirements-dev.txt

带--dev的依赖会被写进 pyproject.toml 的dependency-groups配置块,uv sync默认会装 dev 组,生产部署时用uv sync --no-dev跳过。

3.4 处理 git 依赖、私有包和本地包

老项目里总有几朵奇葩:依赖某个没发 PyPI 的 Git 仓库、公司私有制品库的包、以及本地一个共享目录的包。

Git 依赖:

uv add "git+https://github.com/someone/mylib.git@v0.3.1"

本地目录包(假设项目里有一个 shared/ 目录):

uv add --editable ./shared/lib

注意 editable 模式会把你本地改动实时反应到项目里,开发调试阶段很有用。但如果共享包已经发布到私有源,还是建议用私有源的方式,锁定更可靠。

私有源配置:

在 pyproject.toml 里补充:

[[tool.uv.index]] name = "internal" url = "https://packages.example.com/simple" default = true

还可以用环境变量覆盖UV_INDEX_URL,避免把敏感地址写进仓库。多源并存时的解析顺序,UV 文档里有,核心记住:默认源配了default = true会优先,需要补充源用[[tool.uv.index]]不加 default 即可。

3.5 锁定依赖并验证项目可运行

依赖加完后,执行真正的锁定步骤:

uv lock uv sync

uv lock会解析整棵依赖树,生成uv.lock。这个文件一定要提交进 git,它记录的不只是直接依赖,还包含每一个传递依赖的精确版本、来源、哈希,跨平台通用。之后任何人执行uv sync,拿到的环境和你的完全一致。

验证迁移是否成功,不要只在根目录跑个 import,要按项目的真实运行路径来。Django 项目就跑迁移加冒烟请求,FastAPI 项目就启动服务打一个 health endpoint,脚本项目就直接跑主脚本:

uv run python manage.py migrate uv run python manage.py runserver uv run python main.py

还有一种隐蔽问题:项目里某些代码依赖了requirements.txt里没写,但恰好环境里残留着别的包。这种"隐式依赖"在旧环境里跑得欢,迁到 UV 干净环境立刻暴露。遇到 import 报错,逐个用uv add补上,别急着骂 resolver。

4. 迁移路上的坑与排查链路

4.1 版本解析冲突:旧依赖的通配符魔咒

旧项目最喜欢写numpy>=1.20这种宽松约束。以前 pip 贪快,能装上就行。UV 的 resolver 更严格,它要找到一个在项目所有约束下都能同时满足的组合,旧项目里脏约束一碰撞,直接报ResolutionError。

我实际遇到的一次:项目里pandas>=1.5,另一个包又锁pandas<2.0,同时 numpy 顶层约束是>=1.21。换成 UV 后,它把三方约束拿到一起解,发现某个旧 pandas 版本和 numpy 2.x 不兼容了,直接卡住。

排查方法我总结成三步:

  1. 跑uv lock看完整报错,定位是哪几个包互相打架
  2. 打开uv tree(新版支持)或用uv pip freeze对照现有环境,看当前装的版本组合
  3. 用uv add "pandas==1.5.3"这种精确版本把冲突钉死,从顶往下,逐个收敛

经验是一旦 resolver 报错,不要尝试放宽更多约束去"碰运气",大概率越放越乱。正确动作是找到那个引发问题的老包,同步到最新的兼容版本,让它和相邻依赖达成一致。

4.2 Python 版本解释器错位的经典现场

旧项目在系统里可能同时有 Python 3.8、3.9、3.11,pyproject 里写的是>=3.10,但因为 conda 里残留的 base 环境是 3.8,uv sync可能找到 3.8 或干脆全部解析失败,因为部分新包不支持旧版本。

这个坑的现象很迷惑:uv python find能看到好几个版本,但 resolver 不按你想的选。解决办法是显式钉死.python-version:

uv python pin 3.11

之后uv sync会优先用 3.11,如果本机没有对应解释器,UV 会自动下载安装,或者你也可以手动uv python install 3.11。

另外,注意requires-python和.python-version的关系:前者是项目声明的最低/最高版本,后者是当前开发环境实际使用的版本。团队协作时,.python-version要提交进 git,CI 才能复现同样环境。

4.3 Windows 上 C 扩展构建失败

老项目的 C 扩展包(尤其 psycopg2、lxml、gevent 这类)在 Windows 上经常要编译。以前用 conda 能直接拿到预编译包,切换到 UV 后,默认从 PyPI 拉包,很多包的 Windows 轮子不一定齐全,就触发源码构建,然后因为没有 VC 编译工具链而报错。

这里的排查分两类:

  • 缺 MSVC 构建工具:去装 Visual Studio Build Tools,勾选“使用 C++ 的桌面开发”组件
  • 其实有轮子但不走:指定镜像源。国内环境经常需要配置:
uv add "lxml>=4.9" --index-url https://pypi.tuna.tsinghua.edu.cn/simple

但 UV 的项目模式里源配置是 pyproject 级别的,临时命令用--index-url覆盖更省事。长期来看,我建议在 pyproject.toml 的[tool.uv]里统一配extra-index-url补充可用的镜像源,比如:

[tool.uv] extra-index-url = ["https://mirrors.cloud.tencent.com/pypi/simple"]

这样开发者不需要各自配置,环境一致。

4.4 私有源登录与凭据配置

公司私有 PyPI 一般有认证。老项目往往把账号密码写在.pypirc或者环境变量里,迁到 UV 项目模式后发现uv sync无法认证。

UV 的机制比较简洁:支持UV_INDEX_URL、UV_EXTRA_INDEX_URL环境变量,也支持在 pyproject 里配 index 时带 username/password,但明文放进仓库是找死。我推荐的做法:

  • CI 里用 secrets 注入UV_INDEX_URL或UV_EXTRA_INDEX_URL环境变量
  • 本地开发用.env文件加UV_INDEX_URL=http://user:pass@host/simple,该文件 gitignore
  • 或者配置 keyring,UV 支持读取系统凭据存储

如果公司还要求证书,记得给 UV 配UV_CA_BUNDLE环境变量指向 CA 文件,这个在 Linux 服务器上尤其常见。

5. VSCode 里配置 UV 环境

5.1 选择解释器

迁移完项目,开发环境也要跟上。VSCode 的 Python 插件默认会扫描项目根目录下的.venv,但你有时打开旧窗口,它还是指着系统 Python,因此第一步永远是主动选择解释器。

Ctrl+Shift+P->Python: Select Interpreter,选择.venv路径下的 Python。

如果列表里没有,点“输入解释器路径”,手动指定:

  • Windows:.venv\Scripts\python.exe
  • macOS / Linux:.venv/bin/python

选择之后打开一个 Python 文件,右下角应该会显示解释器版本和.venv标识。

5.2 settings.json 推荐配置

为了让项目成员忽略这些手动步骤,仓库里可以直接提交一份.vscode/settings.json:

{ "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python", "python.terminal.activateEnvironment": true, "python.linting.enabled": true, "python.linting.pylintEnabled": true, "terminal.integrated.env.windows": { "Path": "${workspaceFolder}\\.venv\\Scripts;${env:Path}" }, "terminal.integrated.env.linux": { "PATH": "${workspaceFolder}/.venv/bin:${env:PATH}" } }

注意 Windows 的 Path 分隔符,用反斜杠,引号字符串里再转义一次。我见过有人直接把 Linux 的 PATH 写法搬到 Windows,终端里 activate 脚本全失灵。

VSCode 的 Python 插件在发现 pyproject.toml 后,如果选择依赖是用 UV 管理的,还会提示安装 Pylance 的依赖分析支持。实测下来,Pylance 能正确识别 pyproject 里的依赖,代码补全和类型检查体验提升一个档次。

5.3 uv sync 后刷新

日常开发中,uv add之后要记得重新执行uv sync,VSCode 里的 Pylance 索引才会更新。如果代码里 import 还很红,多半是索引没刷新。我在项目里加了这样的 npm 脚本习惯:uv add xxx && uv sync,一步到位。

另外一个 Mac/Windows 通吃的组合,配置成 VS Code 任务:

{ "label": "uv sync", "command": "uv sync", "type": "shell", "problemMatcher": [] }

配好后,收到“环境已变更”的通知,一键同步,比在终端手动敲舒服多了。

6. 团队协作:把 UV 固化到工作流

6.1 该提交什么文件

迁移完成后,仓库里应该新增/保留下这些文件:

文件状态说明
pyproject.toml新增/更新项目声明与依赖
uv.lock新增完整锁定文件
.python-version新增解释器版本钉死
requirements.txt可保留但不再使用建议迁完稳定后删除
setup.py如无必要可清理纯应用项目建议删除,库项目另行评估

我见过的反面教材是:pyproject 都建好了,但 CI 里还在用pip install -r requirements.txt。迁移就要彻底,基本信条:所有环境的创建和依赖安装,只通过uv sync触发,不再并行维护两套系统。

6.2 CI 里用 UV

GitHub Actions 官方推荐用 setup-uv:

- uses: astral-sh/setup-uv@v6 with: uv-version: "0.5.0" - run: uv sync --frozen - run: uv run pytest

--frozen表示完全按uv.lock锁定文件来干活,不动锁文件本身。如果某个依赖的版本在 lock 和 pyproject 之间不一致,uv sync --frozen会直接报错,正好暴露有人手动改了 pyproject 但没重新锁。

其他 CI 系统类似:先安装 UV(用官方脚本或下载二进制),然后uv sync --frozen,后续命令都用uv run执行。

缓存配置也很关键,GitHub Actions 里有uv-cache缓存模块可以用,把~/.cache/uv缓存上,整个 CI 提速非常明显。

6.3 uv lock --check 前置检查

有人手改 pyproject.toml 却忘记uv lock,是团队协作最烦的问题。我建议在 CI 加一步:

uv lock --check

该命令只检查 pyproject.toml 和 uv.lock 是否一致,不生成新文件。一旦不一致直接 fail,让开发者回到本地执行uv lock再提交。

再加一层保险,pre-commit hook:

repos: - repo: https://github.com/astral-sh/uv-pre-commit rev: v0.5.0 hooks: - id: uv-lock-check

这样本地 commit 阶段就会拦截。配合uv sync日常习惯,团队成员基本不会再出现“环境对不上”的尴尬。

个人建议,兼容模式和项目模式可以并行掌握,但接手旧项目时尽快统一到项目模式,否则等于左手 pip 右手 uv,最终还是陷入双份混乱。

关于迁移之后

我迁完旧项目的那个星期,最大的感受不是"装得更快"“切换更顺”,而是心里的确定感知道项目依赖长什么样、锁在哪、为什么装上这个版本。这比任何速度指标都重要。你手上如果也有那种三四年没动过的老项目,建议挑一个非核心的服务先试一次完整迁移,流程跑通了再铺开。UV 的学习曲线不算陡,真正陡的是旧项目里那一堆被时间酿出来的隐式依赖和非法版本约束,慢慢摊开看,一个一个收服就好。

返回列表