“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.txtdev 依赖放 requirements-dev.txt版本有时候钉死有时候不钉解释器版本靠 README 里一句话。UV 出现之后这些东西被统一收进 pyproject.toml 和 uv.lock人眼和机器都能看懂。1.2 一个文件管到底用 pip 时代项目元数据是 setup.py / setup.cfg运行时依赖是 requirements.txtdev 依赖再来一个文件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.lockPython 版本管理不支持支持不支持需 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 --idastral-sh.uv建议装完立刻跑一次uv --version确认。之后升级直接uv self update不需要重新走安装脚本。安装后二进制默认放到用户目录Windows 是C:\Users\Administrator\AppData\Local\uvmacOS 是~/.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全员统一解释器版本。第二层项目虚拟环境切换。每个项目有自己的.venvuv 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 numpy2.0 python -c import numpy这会临时构造一个隔离环境脚本跑完就丢对 macOS 上经常写一次性数据分析脚本的人来说简直是救星。2.4 缓存目录和常用维护命令UV 的全局缓存是提速的关键但时间久了也挺占空间。Windows 上默认在C:\Users\Administrator\AppData\Local\uv\cachemacOS 在~/Library/Caches/uv。查看和清理uv cache dir uv cache cleanuv 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 之前先搞清楚旧项目现在靠什么活着。我归纳下来基本逃不过这四种情况只有requirements.txt和requirements-dev.txt没有 pyproject.toml有setup.py可能是传统库项目有 pyproject.toml但用的是 setuptools 后端更惨一点的pipenv 的 Pipfile先别急着删任何文件。迁移的一个铁律是不要破坏现有可运行状态新老方式共存一段时间确认无问题再清理。我把 pyproject.toml 之外的所有声明文件备份进legacy/目录而不是直接扔 trash这样随时能回退。看一下requirements.txt里的版本约束风格也重要。老项目常见两种完全钉死requests2.31.0或者宽松requests2.0。后者在 UV 的严格 resolver 下可能引发解析问题后面第 4.1 节专门讲。3.2 生成 pyproject.toml在项目根目录执行uv init --bare --python3.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 githttps://github.com/someone/mylib.gitv0.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 syncuv 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 版本解析冲突旧依赖的通配符魔咒旧项目最喜欢写numpy1.20这种宽松约束。以前 pip 贪快能装上就行。UV 的 resolver 更严格它要找到一个在项目所有约束下都能同时满足的组合旧项目里脏约束一碰撞直接报ResolutionError。我实际遇到的一次项目里pandas1.5另一个包又锁pandas2.0同时 numpy 顶层约束是1.21。换成 UV 后它把三方约束拿到一起解发现某个旧 pandas 版本和 numpy 2.x 不兼容了直接卡住。排查方法我总结成三步跑uv lock看完整报错定位是哪几个包互相打架打开uv tree新版支持或用uv pip freeze对照现有环境看当前装的版本组合用uv add pandas1.5.3这种精确版本把冲突钉死从顶往下逐个收敛经验是一旦 resolver 报错不要尝试放宽更多约束去碰运气大概率越放越乱。正确动作是找到那个引发问题的老包同步到最新的兼容版本让它和相邻依赖达成一致。4.2 Python 版本解释器错位的经典现场旧项目在系统里可能同时有 Python 3.8、3.9、3.11pyproject 里写的是3.10但因为 conda 里残留的 base 环境是 3.8uv sync可能找到 3.8 或干脆全部解析失败因为部分新包不支持旧版本。这个坑的现象很迷惑uv python find能看到好几个版本但 resolver 不按你想的选。解决办法是显式钉死.python-versionuv python pin 3.11之后uv sync会优先用 3.11如果本机没有对应解释器UV 会自动下载安装或者你也可以手动uv python install 3.11。另外注意requires-python和.python-version的关系前者是项目声明的最低/最高版本后者是当前开发环境实际使用的版本。团队协作时.python-version要提交进 gitCI 才能复现同样环境。4.3 Windows 上 C 扩展构建失败老项目的 C 扩展包尤其 psycopg2、lxml、gevent 这类在 Windows 上经常要编译。以前用 conda 能直接拿到预编译包切换到 UV 后默认从 PyPI 拉包很多包的 Windows 轮子不一定齐全就触发源码构建然后因为没有 VC 编译工具链而报错。这里的排查分两类缺 MSVC 构建工具去装 Visual Studio Build Tools勾选“使用 C 的桌面开发”组件其实有轮子但不走指定镜像源。国内环境经常需要配置uv add lxml4.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_URLhttp://user:passhost/simple该文件 gitignore或者配置 keyringUV 支持读取系统凭据存储如果公司还要求证书记得给 UV 配UV_CA_BUNDLE环境变量指向 CA 文件这个在 Linux 服务器上尤其常见。5. VSCode 里配置 UV 环境5.1 选择解释器迁移完项目开发环境也要跟上。VSCode 的 Python 插件默认会扫描项目根目录下的.venv但你有时打开旧窗口它还是指着系统 Python因此第一步永远是主动选择解释器。CtrlShiftP-Python: Select Interpreter选择.venv路径下的 Python。如果列表里没有点“输入解释器路径”手动指定Windows:.venv\Scripts\python.exemacOS / 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 syncVSCode 里的 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 里用 UVGitHub Actions 官方推荐用 setup-uv- uses: astral-sh/setup-uvv6 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 hookrepos: - repo: https://github.com/astral-sh/uv-pre-commit rev: v0.5.0 hooks: - id: uv-lock-check这样本地 commit 阶段就会拦截。配合uv sync日常习惯团队成员基本不会再出现“环境对不上”的尴尬。个人建议兼容模式和项目模式可以并行掌握但接手旧项目时尽快统一到项目模式否则等于左手 pip 右手 uv最终还是陷入双份混乱。关于迁移之后我迁完旧项目的那个星期最大的感受不是装得更快“切换更顺”而是心里的确定感知道项目依赖长什么样、锁在哪、为什么装上这个版本。这比任何速度指标都重要。你手上如果也有那种三四年没动过的老项目建议挑一个非核心的服务先试一次完整迁移流程跑通了再铺开。UV 的学习曲线不算陡真正陡的是旧项目里那一堆被时间酿出来的隐式依赖和非法版本约束慢慢摊开看一个一个收服就好。