首页
/ 从 pip 与 pip-tools 迁移到 uv 项目:requirements 工作流到 pyproject.toml 与 uv.lock 的完整实战指南

从 pip 与 pip-tools 迁移到 uv 项目:requirements 工作流到 pyproject.toml 与 uv.lock 的完整实战指南

2026-09-06 11:41:22作者:秋阔奎Evelyn

本文基于 uv 官方迁移指南 pip-to-project.md 展开,讲解如何把以 pippip-toolsrequirements 文件为核心的依赖管理工作流,完整迁移到基于 pyproject.tomluv.lock 的 uv 项目工作流。读完后你将掌握:pip/pip-tools 工作流的工作原理与痛点、requirements.inpyproject.toml 的逐项映射方法、用 uv add 保留已锁定版本的具体命令,以及开发依赖组、平台特定约束、本地路径与 Git 源依赖的导入技巧,并能结合源码理解 uv add 处理约束与依赖来源的底层机制。

一、理解 pip 工作流:迁移前必须先弄懂的原点

迁移的前提是准确理解旧工作流的每一个环节。本节完整还原 pippip-tools 的协作方式,这也是后续 uv 命令一一映射的对照基准。

1.1 项目依赖:命令式安装与虚拟环境

pip 的核心是使用命令式(imperative)安装:

$ pip install fastapi

包会被安装到 pip 所在的 Python 环境——可能是某个虚拟环境,也可能是系统全局 Python 环境。之后就可以运行引用该包的脚本(example.py):

import fastapi

最佳实践是为每个项目单独创建虚拟环境,避免多个项目的包互相污染:

$ python -m venv
$ source .venv/bin/activate
$ pip ...

这个"手动激活环境"的模式,正是 uv 项目工作流要取代的核心体验(见 第四节)。

1.2 Requirements 文件:从共享依赖到版本锁定

pip 支持从文件批量安装依赖,便于项目共享:

fastapi
$ pip install -r requirements.txt

注意这里的 fastapi 并没有锁定到具体版本——不同开发者安装到的 fastapi 版本可能各不相同。pip-tools 就是为改善这一点而生的。

使用 pip-tools 时,通过文件扩展名区分"依赖声明"与"锁定结果"。例如项目依赖 fastapipydantic 时,在 requirements.in 中声明:

fastapi
pydantic>2

其中 pydantic>2 是版本约束——只允许 2.0.0 之后的版本;而 fastapi 无约束,任意版本可用。接着执行编译,生成锁定文件:

$ pip-compile requirements.in -o requirements.txt
annotated-types==0.7.0
    # via pydantic
anyio==4.8.0
    # via starlette
fastapi==0.115.11
    # via -r requirements.in
idna==3.10
    # via anyio
pydantic==2.10.6
    # via
    #   -r requirements.in
    #   fastapi
pydantic-core==2.27.2
    # via pydantic
sniffio==1.3.1
    # via anyio
starlette==0.46.1
    # via fastapi
typing-extensions==4.12.2
    # via
    #   fastapi
    #   pydantic
    #   pydantic-core

此时所有版本约束都是精确的(==),每个包只能使用唯一版本。上述示例既可以用 uv pip compile 生成,也可以用 pip-toolspip-compile 生成——这正是 uv 兼容旧工作流的体现。

另一种较少见的做法是先用 pip freeze 导出已安装版本:

$ pip install -r requirements.in
$ pip freeze > requirements.txt
annotated-types==0.7.0
anyio==4.8.0
fastapi==0.115.11
idna==3.10
pydantic==2.10.6
pydantic-core==2.27.2
sniffio==1.3.1
starlette==0.46.1
typing-extensions==4.12.2

编译完成后,锁定文件与 requirements.in 一起提交到版本控制,随项目分发。使用者则通过 pip install -r requirements.txt 安装完全一致的版本。

1.3 开发依赖:一组依赖就需要一个文件

requirements 文件格式一次只能描述一组依赖。因此开发依赖这类"额外的依赖组"必须放在单独的文件中:

-r requirements.in
-c requirements.txt

pytest

这里有两个关键行:

  • -r requirements.in:把基础依赖引用进来,确保开发环境同时考虑全部依赖;
  • -c requirements.txt:用约束固定版本,保证编译出的 requirements-dev.txtrequirements.txt 使用相同版本。

有一个常见做法值得注意:很多人会直接用 -r requirements.txt 代替"-r requirements.in + -c requirements.txt"的组合。两者产生的包版本结果相同,区别在于注释标注:组合写法能区分直接依赖(标注 -r requirements.in)与间接依赖(仅标注 -c requirements.txt)。

编译后的开发依赖文件(节选):

pytest==8.3.5
    # via -r requirements-dev.in
iniconfig==2.0.0
    # via pytest
packaging==24.2
    # via pytest
pluggy==1.5.0
    # via pytest

同样提交到版本控制,协作者通过 pip install -r requirements-dev.txt 获得一致的开发环境。

1.4 平台特定依赖:每个平台一份锁定文件

pippip-tools 编译出的锁定文件只能在生成它的平台上使用。这对需要同时支持 Windows、macOS、Linux 的项目是个大麻烦。

tqdm 为例:

tqdm

在 Linux 上编译结果:

tqdm==4.67.1
    # via -r requirements.in

在 Windows 上编译结果:

colorama==0.4.6
    # via tqdm
tqdm==4.67.1
    # via -r requirements.in

coloramatqdm 的 Windows 独占依赖。因此用 pip 工作流时,项目必须为每个受支持平台各维护一份锁定文件。

uv 的解决方案是通用(universal)解析:一次为多个平台编译,所有平台共用一份 requirements.txt,环境信息通过 PEP 508 标记表达:

$ uv pip compile --universal requirements.in
colorama==0.4.6 ; sys_platform == 'win32'
    # via tqdm
tqdm==4.67.1
    # via -r requirements.in

详见 universal resolution 概念文档。使用 pyproject.toml + uv.lock 时,锁文件默认就是这个模式。

二、迁移目标:pyproject.toml 与 uv.lock 分别取代什么

2.1 pyproject.toml 取代 requirements.in

pyproject.toml 是 Python 项目的标准化元数据文件。它取代 requirements.in,支持表达任意数量的依赖分组,同时集中管理构建系统、工具配置等元数据。

前述 requirements.inrequirements-dev.in 的内容,等价于:

[project]
name = "example"
version = "0.0.1"
dependencies = [
    "fastapi",
    "pydantic>2"
]

[dependency-groups]
dev = ["pytest"]

对比可见映射关系非常直接:

旧工作流 uv 项目工作流
requirements.in [project.dependencies]
requirements-dev.in-r 引用基础依赖 + 新依赖) [dependency-groups] 下的 dev
依赖来源(路径、-e、Git URL) [tool.uv.sources]

2.2 uv.lock 取代 requirements.txt(以及所有平台的副本)

uv 用专属格式的锁文件 uv.lock 锁定包版本。该格式为 uv 定制,支持 requirements.txt 无法表达的高级特性。关键差异:

  • 自动维护:添加依赖时锁文件自动创建并更新,也可用 uv lock 显式重建;
  • 多分组:单个锁文件即可覆盖开发依赖等任意依赖组,无需为每组各生成一个 requirements.txt
  • 天生通用uv.lock 始终是多平台通用的(universal),不再需要每个平台一份锁定文件。无论开发者的机器是什么系统,大家拿到的是同一份一致、锁定的依赖版本;
  • 表达能力更强:支持把包固定到特定索引(pinning a package to an index)这类 requirements.txt 无法表达的概念。

如果只需为部分平台锁定,可用 tool.uv.environments 设置限制解析与锁文件的环境范围。锁文件更多细节见 lockfile 文档

三、导入 requirements 文件:迁移的核心操作

3.1 初始化项目并导入基础依赖

第一步,若还没有 pyproject.toml,先用 uv init 创建:

$ uv init

然后最简单的导入方式就是用 uv add

$ uv add -r requirements.in

但这里有一个关键细节:requirements.in 不锁定精确版本,所以 uv 会重新求解这些包的新版本。如果你想切换到 uv 时依赖版本零变化,正确做法是把你已有的锁定版本作为约束传入:

$ uv add -r requirements.in -c requirements.txt

这样生成的 uv.lock 会保留你现有的版本。uv 集成测试套件中有专门用例验证这一行为——add_requirements_file_constraints 测试确认:传入 -c requirements.txt 后,旧版本(如 anyio==3.7.1flask==1.1.4)被完整保留,且约束本身不会被写入 pyproject.tomluv.lock——它只影响本次求解结果。

3.2 导入平台特定约束

如果平台特定依赖已被编译成多份文件(如 requirements-win.txtrequirements-linux.txt),仍然可以迁移到通用锁文件,但不能直接-c 引用这些文件——它们没有描述环境的标记(marker),合并时会互相冲突。

解决办法是用 uv pip compile 给现有文件补上标记。例如对 Windows 版本:

$ uv pip compile requirements.in -o requirements-win.txt --python-platform windows --no-strip-markers

补上标记后的输出(注意 colorama 的 Windows 标记):

colorama==0.4.6 ; sys_platform == 'win32'
    # via tqdm
tqdm==4.67.1
    # via -r requirements.in

使用 -o 指向已存在的输出文件时,uv 会在可能的情况下把版本约束为与现有文件一致,从而不改变已锁定的版本。

对其余平台,只需替换 --python-platform-o 参数值(如 linuxmacos),逐文件补标记。全部转换完成后,用 uv add 一次导入:

$ uv add -r requirements.in -c requirements-win.txt -c requirements-linux.txt

3.3 导入开发依赖文件

导入开发依赖时给 uv add--dev 标志:

$ uv add --dev -r requirements-dev.in -c requirements-dev.txt

有一个常见坑:如果 requirements-dev.in 内部通过 -r 引用了父文件 requirements.in,导入前必须把这类行剥掉,否则基础依赖会被重复添加进 dev 依赖组。官方指南给出的做法是用 sed 删除以 -r 开头的行,再管道给 uv add-r - 表示从标准输入读取):

$ sed '/^-r /d' requirements-dev.in | uv add --dev -r - -c requirements-dev.txt

除了 dev 组,uv 支持任意命名的依赖组。例如构建文档用的独立依赖,可导入到名为 docs 的组:

$ uv add -r requirements-docs.in -c requirements-docs.txt --group docs

在源码层面,add.rs 中的 DependencyType 枚举定义了 --dev(映射到 dev 组)与 --group <name>DependencyType::Group)两条路径,二者都会把依赖写入 pyproject.toml[dependency-groups] 表并同步锁文件。

3.4 导入本地路径与 Git 依赖来源

requirements.in 中包含本地路径或 Git 仓库依赖时:

./path-dep
-e ./editable-path-dep
git-dep @ git+https://github.com/astral-sh/git-dep

uv 不会把 URL 原样写进 dependencies,而是把它们映射到 pyproject.tomldependency sources[tool.uv.sources] 表):

[project]
dependencies = [
    "path-dep",
    "editable-path-dep",
    "git-dep",
]

[tool.uv.sources]
path-dep = { path = "./path-dep" }
editable-path-dep = { path = "./editable-path-dep", editable = true }
git-dep = { git = "https://github.com/astral-sh/git-dep" }

对应到源码实现:add.rs 中的 resolve_requirement 函数(约 L1286 起)调用 Source::from_requirement,把 Requirement 的来源(目录路径、editable、Git 等)解析为 Source 枚举,随后通过 processed_requirement.clear_url() 清除 PEP 508 原始 URL——这就是为什么最终 dependencies 里只剩包名、而来源信息独立存放在 [tool.uv.sources] 中。对于 Git 依赖,如果引用了无法自动解析的 rev/tag/branch,uv add 会明确报错并提示使用 --tag--branch--rev 显式指定(或改用 --raw-sources)。

四、项目环境:uv 不再围绕"激活"的虚拟环境

迁移后最后一个工作流差异是环境管理。pip 的心智模型是"当前激活的虚拟环境",而 uv 的心智模型是每个项目一个专属环境

  • uv 为每个项目在 .venv 目录中维护专用虚拟环境,自动管理——执行 uv add 等命令时环境会随项目依赖自动同步;
  • 在环境中执行命令的首选方式是 uv run
$ uv run pytest

每次 uv run 之前,uv 都会先校验锁文件与 pyproject.toml 一致、环境内容与锁文件一致,然后才执行命令。这意味着你无需任何手动干预,命令始终运行在一致、已锁定的环境中;

  • 也可以用 uv sync 显式创建项目环境,例如供编辑器使用;
  • 一个值得注意的默认行为:在项目中,uv 优先使用项目目录下的 .venv,并忽略 VIRTUAL_ENV 环境变量声明的激活环境。如确实想用激活环境,可用 --active 标志显式选择。

更多细节见 项目环境文档

五、迁移检查清单与延伸阅读

把整条迁移路径压缩成一张检查清单:

  1. uv init 创建 pyproject.toml
  2. uv add -r requirements.in -c requirements.txt 导入基础依赖(用 -c 保住旧版本);
  3. uv add --dev -r ... -c ...(必要时先 sed 剥掉 -r 行)导入开发组;其他组用 --group <name>
  4. 平台锁定文件先用 uv pip compile --python-platform <平台> --no-strip-markers 补标记,再合并导入;
  5. 路径/-e/Git 依赖自动落入 [tool.uv.sources]
  6. 日常工作切换到 uv run / uv sync,不再手动 source .venv/bin/activate

完成迁移后,建议继续阅读 uv 项目概念总览,深入理解项目布局、锁文件与依赖模型;本文引用的迁移指南原文见 pip-to-project.md

登录后查看全文
热门项目推荐
相关项目推荐