Python 项目迁移到 uv:经验小结与可复用工作流

迁移前要确定的事

  1. Python 版本区间:项目实际验证过的版本
  2. 非默认来源:私有源地址、--extra-index-url、--trusted-host、裸 wheel URL
  3. 当前版本快照:pip freeze > baseline-freeze.txt,迁移后拿它做对照,出问题时也能回滚
  4. 搞清楚两个容易混的概念:.python-version 和 requires-python
    • .python-version:由 uv python pin 生成,决定 uv run、uv sync 用哪个本地解释器
    • requires-python:写在 pyproject.toml 里,决定 uv lock 解析时覆盖的版本区间。区间越窄,锁文件越小

初始化 uv 项目

已有 pyproject.toml 则跳过 uv init

1
2
uv init --no-readme
uv python pin 3.12
1
2
[project]
requires-python = ">=3.12,<3.13"

最好把 .python-version 提交,用 git 管理。

一个容易踩的坑:uv pip install 不读 requires-python,只认当前激活环境或当前目录下的 .venv。所以用 uv pip 系列命令之前,先跑一遍 uv sync 把 .venv 建好。

导入 requirements.txt

requirements.txt 中的关键包最好锁定精确版本:

1
2
3
4
5
# 不推荐
vlt-xxx-aws

# 推荐
vlt-xxx-aws==0.1.53

为什么要精确?一是私有源上的包往往没有严格的 semver 保证,版本号跳动的含义跟 PyPI 上的包不是一回事;二是如果内部包跟 PyPI 上某个公开包重名,不锁精确版本时解析器可能会取到完全没预料到的来源。这类问题排查起来很费时间,因为表现出来就是”依赖版本对不上”,但根源是包来源搞错了。

导入命令:

1
uv add -r requirements.txt

uv add -r 会重写 pyproject.toml 并重新解析锁文件。uv sync 只按现有锁文件同步环境,不能替代迁移导入。

私有源踩过的坑

举一个真实场景。配好私有源之后,uv add 突然报这样的错:

1
2
3
4
No solution found when resolving dependencies:
`-> Because there is no version of psutil==6.1.0 ...
hint: `psutil` was found on http://xxxx-pip.xxxx.lan/simple, but not at the requested version
A compatible version may be available on a subsequent index ...

第一反应往往是”这个包是不是被删了”,但其实包还在,只是版本不全——原因出在 uv 默认的 index-strategy = "first-index" 策略上:uv 按索引顺序查找,包名一旦在某个索引里找到,就只从这个索引解析这个包,不会再去后面的索引找更全的版本。这里的坑是,内部镜像代理了 psutil,但只同步了部分版本,uv 找到包名就停手了,根本不会意识到后面还有一个版本更全的索引。

索引配置长这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[[tool.uv.index]]
name = "private"
url = "http://xxo-private-pip.xxxx.lan:9090/simple"
default = true

[[tool.uv.index]]
name = "internal-wheels"
url = "http://172.xx.xxx.229:8899/simple/"

[[tool.uv.index]]
name = "xxp"
url = "http://xxp-pip.xx.lan/simple"

[tool.uv]
allow-insecure-host = [
"xxo-private-pip.xxxx.lan",
"172.xx.xxx.229",
"xxp-pip.xx.lan",
]

几点要注意:

  • default = true 会禁用 PyPI,并把这个索引放到已配置索引里优先级最低的位置。
  • requirements.txt 里的 --trusted-host 对应的是 allow-insecure-host,不是索引配置里的 trusted 字段——这两个名字太像,很容易配错地方。
  • 能配出有效 TLS 证书的话,优先修证书,allow-insecure-host 只应该是长期方案里的例外,不是常态。

解决刚才那个 psutil 问题,有两种思路:

一种是全局关闭 first-index 策略:

1
2
[tool.uv]
index-strategy = "unsafe-best-mxxxh"

这样会跨所有索引找最高版本,但风险也最大——只有当你配置的所有索引都完全可信时才应该这么做,否则相当于把供应链安全性拱手让出去。

更推荐的做法是单包 pin 来源,只解决出问题的那一个包:

1
2
3
4
5
6
[tool.uv.sources]
psutil = { index = "private" }

[[tool.uv.index]]
name = "private"
url = "http://xxxx-private-pip.xxxx.lan:9090/simple"

URL 依赖

uv 不允许 URL 依赖仅作为传递依赖存在,必须在 dependencies 中显式声明,并在 [tool.uv.sources] 里配置来源:

1
2
3
4
5
[project]
dependencies = ["xxx-engine-alarm"]

[tool.uv.sources]
xxx-engine-alarm = { url = "http://.../xxx_engine_alarm-0.0.3-py3-none-any.whl" }

如果第三方包内部也用 URL 声明了同一个依赖,两边必须完全一致(版本、URL、hash),否则会报类似这样的冲突:

1
2
3
error: Requirements contain conflicting URLs for package `xxx-engine-alarm`:
- http://.../xxx_engine_alarm-0.0.3-py3-none-any.whl
- http://.../xxx_engine_alarm-0.0.3-py3-none-any.whl (from a transitive dependency)

看着像是同一个 URL,但差一个字符(比如内部包里写的是旧版本号,或者 hash 对不上)都会触发。遇到这种报错,先去翻第三方包自己声明的依赖来源,跟你项目里写的逐字比对。

过渡期兜底

如果时间紧,可以先用两条命令临时把依赖跑起来:

1
2
uv add -r requirements.txt --frozen       # 跳过锁文件更新,临时导入依赖声明
uv pip install -r requirements.txt # 完全绕开项目解析

这两条本质上相同——都是”先让代码跑起来,锁文件的事后面再说”。--frozen 不会生成可信的 uv.lock;uv pip install 干脆不走项目解析这条路,执行前记得确认 .venv 已经建好。这两条命令用完之后,一定要回头补上正式的 uv lock。

收尾验证

重新生成锁文件:

1
uv lock

干净环境复现:

1
2
3
4
5
6
7
# Windows
Remove-Item -Recurse -Force .venv
# Unix/macOS
rm -rf .venv

uv sync
uv run python -c "import sys; print(sys.version)"

对照 baseline-freeze.txt 抽查关键包版本

在 CI 或另一台干净机器上再跑一次 uv sync,确认不依赖本机缓存。最终再执行一遍工程的测试或者服务,确保没有报错,这才是根本的。

如果要回滚

迁移过程中如果卡住,回滚比继续排查更划算的情况不少见。核心是保留住两样东西:baseline-freeze.txt 和原来的 requirements.txt。

1
2
3
4
rm -rf .venv
python -m venv .venv
source .venv/bin/activate # Windows 用 .venv\Scripts\activate
pip install -r requirements.txt

回滚不需要动 pyproject.toml 和 uv.lock——留着它们,下次再迁移时可以从上次中断的地方继续。

uv.lock 体积过大

最常见的原因是 requires-python 区间过宽。修复方式是收窄区间后重新生成锁文件:

1
2
[project]
requires-python = ">=3.12,<3.13"
1
uv lock

另一个不那么明显的原因是索引配置太多:配置的源越多,解析器给每个包做候选校验时要跨的源就越多,锁文件里记录的候选信息也会跟着膨胀。如果收窄 requires-python 之后体积还是没降下来,可以回头看看 [[tool.uv.index]] 是不是配多了,有没有可以合并或去掉的。


uv 常用命令速查

下面按“项目接口”和“uv pip 兼容接口”分类。日常项目优先使用项目接口:它会一起维护 pyproject.toml、uv.lock 和项目虚拟环境;uv pip 则直接操作某个虚拟环境,更适合迁移旧的 requirements.txt 或兼容原有 pip 工作流。

1. 安装、版本与帮助

1
2
3
4
uv --version                    # 查看 uv 版本
uv self update # 更新 uv(独立安装方式支持时)
uv help # 查看顶层帮助
uv help sync # 查看具体命令帮助

2. Python 版本管理

1
2
3
4
uv python list                  # 查看可用/已安装的 Python
uv python install 3.12 # 安装 Python 3.12 的最新补丁版本
uv python pin 3.12 # 当前项目生成 .python-version
uv python find # 查看 uv 将使用的解释器

uv python pin 只固定本地默认解释器;项目支持哪些 Python 版本,仍由 pyproject.toml 中的 requires-python 决定。

3. 创建项目和虚拟环境

1
2
3
4
uv init                         # 在当前目录创建项目
uv init --no-readme # 创建项目但不生成 README
uv venv # 单独创建 .venv,适合非项目式工作流
uv venv --python 3.12 # 使用指定 Python 创建虚拟环境

已有 pyproject.toml 时不需要重复执行 uv init,直接补齐 [project] 配置即可。

4. 项目依赖管理

1
2
3
4
5
6
7
8
uv add requests                 # 添加运行时依赖
uv add "requests>=2.32,<3" # 添加带版本范围的依赖
uv add --dev pytest # 添加到 dev 依赖组
uv add --group lint ruff # 添加到指定依赖组
uv add -r requirements.txt # 从 requirements 文件导入
uv remove requests # 移除直接依赖
uv sync # 按 pyproject.toml 和 uv.lock 同步 .venv
uv sync --dev # 同步并包含 dev 依赖

常用选项:

  • --upgrade:允许升级依赖,而不是尽量沿用锁文件中的版本。
  • --no-sync:修改项目依赖或锁文件,但暂不更新 .venv。
  • --locked:要求 uv.lock 已经是最新,否则报错。
  • --frozen:不检查或更新锁文件,适合明确知道后果的临时场景。

5. 锁文件、依赖树和运行命令

1
2
3
4
5
6
uv lock                         # 解析并更新 uv.lock
uv lock --check # 检查锁文件是否需要更新
uv tree # 查看当前项目依赖树
uv run python app.py # 在项目环境中运行脚本
uv run pytest # 使用项目环境运行测试
uv run --with rich python -c "import rich" # 临时附加依赖运行一次

uv run 会在运行前检查锁文件和项目环境是否需要同步;CI 中可使用 uv sync --locked,让锁文件不一致时直接失败。

6. 导出、构建和发布

1
2
3
uv export --format requirements.txt --output-file requirements.txt
uv build # 构建 wheel 和源码包到 dist/
uv publish # 发布 dist/ 中的构建产物

如果项目以 uv.lock 作为唯一锁文件,通常不需要同时长期维护一份手写的 requirements.txt;只有部署系统或旧工具要求时再使用 uv export 导出。

7. uv pip 兼容接口

这组命令直接面向虚拟环境,不会像项目接口那样自动维护项目的 pyproject.toml 和 uv.lock:

1
2
3
4
5
6
7
8
uv pip install -r requirements.txt       # 安装,不会主动删除多余包
uv pip sync requirements.txt # 严格同步,删除文件中未列出的包
uv pip compile requirements.in -o requirements.txt # 解析并生成锁定的 requirements
uv pip freeze # 导出当前环境已安装版本
uv pip list # 查看已安装包
uv pip show requests # 查看包信息
uv pip tree # 查看环境依赖树
uv pip check # 检查已安装依赖是否冲突

如果使用这组命令,建议显式指定目标解释器(例如 uv pip install --python .venv ...),或先创建并确认当前目录的 .venv,避免误装到其他环境。