每个项目迭代过几个版本的人大概率都被这行命令埋过雷pip install -r requirements.txt -c constraints.txtrequirements 和 constraints 都写得很规整结果 pip 甩过来一个冲突ResolutionImpossible还算客气有时直接一句Constraints contain packages not installed看完一头雾水。这篇文章想把这个 Python 依赖管理里的高频问题彻底讲明白两个文件分别管什么“constraints 未被满足”为什么会发生以及从报错到修复我实际会走哪几步。做后端、管数据、维护部署流水线的朋友应该都用得上尤其是接手老项目、想安全动底包的时候这套思路几乎是必选项。1. requirements 和 constraints 的真实分工1.1 用点菜和预算理解两个文件把requirements.txt想象成一张点菜单上面列的每道菜pip 都会真的端上来安装constraints.txt更像一张预算卡它不负责加菜只负责限制不管这个包是被你直接点名还是被其他依赖连带引入它的版本都不能超出这个卡面范围。这个类比能解决一半的误解。很多人把 constraints 当成“额外的 requirements”在文件里写下包名版本指望 pip 顺手把它装上——结果发现根本不装或者装上了但约束根本没生效反而引发后续安装失败。举个例子你点了一份 flask它会附带依赖 Werkzeug。如果在 constraints 里限定 Werkzeug 的版本范围pip 只会在范围内挑选一个满足条件的版本但如果 Werkzeug 没有被任何包依赖这个约束就纯粹是个摆设写了等于没写。1.2 pip 解析依赖时到底按什么顺序工作pip 实际做的事情大致分四步先收集-r文件里的根需求再读取每个包的元数据展开整棵传递依赖树接着把-c里的约束加进来作为版本候选集的过滤器最后交给 resolver 寻找一组满足所有条件的版本。从 pip 20.3 开始默认解析器换成了更严格的 backtracking 解析器它会为了凑齐方案反复回退、试探解析行为比老版本更“较真”。好处是很少再出现半装半不装的残缺环境坏处是以前一些“能装上”的组合现在会被直接判死。你看到的冲突报错很多就是这么来的不是文件写错了而是解析器变严格之后旧组合暴露了问题。1.3 一张表看透两者差异维度requirements.txtconstraints.txt是否主动安装是否对直接依赖的作用指定要安装的目标版本只限制版本范围对传递依赖的作用不直接起作用过滤可选的传递依赖版本命令参数-r/--requirement-c/--constraint典型目的定义项目跑起来必须装的包锁定/限制某些包的版本上限这张表的关键是最上面两行requirements 是“要安装的目标”constraints 是“对候选版本的过滤条件”。记住这句话后面排查思路就会清晰很多。一个常见的错误是有人把 constraints 文件用-r参数加载比如pip install -r requirements.txt -r constraints.txt结果约束文件里的所有包都会被真的安装进环境约束直接失效本来想限制传递依赖反而变成往环境里塞了一堆不需要的东西。2. 先把报错文本拆开“constraints 未被满足”到底指什么2.1 两种最典型的报错说法这类问题在报错里通常有两副面孔。一副是直截了当的ResolutionImpossible完整文本类似ERROR: Cannot install requests2.31.0 because these package versions have conflicting dependencies. ERROR: ResolutionImpossible: for help visit https://pip.pypa.io/en/latest/user_guide/#fixing-conflicting-dependencies意思是 resolver 把候选版本都试了一遍找不到一个能同时满足所有要求的组合于是放弃治疗。另一副和 constraints 直接相关提示某个约束没有被满足常见说法类似Constraints contain packages not installed不同 pip 版本文案略有差异。无论文字怎么变含义都一样约束文件里的包在整棵依赖树里找不到对应需求约束悬空了。pip 并不会因为文件里有这个包就主动安装它所以也无法自动满足这个约束。2.2 三类高频触发场景场景 A直接依赖版本钉死约束卡得过窄。# requirements.txt requests2.31.0 # constraints.txt urllib31.21requests 2.31.0 的元数据要求urllib31.21.1,3但 constraints 要求urllib31.21。两个区间没有一个共同的数字resolver 直接放弃。这种场景最常见本质是直接依赖的版本要求和你额外加的约束打架。场景 B写了个项目里根本用不到的包。# requirements.txt flask2.2.5 # constraints.txt pandas1.5.3flask 的整条依赖链里没有 pandaspip 解析完发现 pandas 不会被安装约束行落空。不同 pip 版本里这可能是 warning 也可能是 error但处理逻辑一样要么删掉这行要么真的让 pandas 转正把它写进 requirements.txt。场景 C约束文件根本没被正确加载。比如你在 requirements.txt 里写了-c constraints.txt但文件不在当前工作目录或者在命令行里自信满满写-c ./constraints.txt实际文件在config/子目录下。约束一旦没被加载安装看似成功后续换环境跑一次才暴露问题更隐蔽。还有一种常见误操作就是用-r constraints.txt而不是-c constraints.txt把约束文件当作 requirements 安装所有约束全部失效。2.3 报错速查表报错形态一般原因处理方向ResolutionImpossible、版本组合无解直接依赖要求与 constraints 交集为空调整 constraints 范围或升级/降级对应直接依赖Constraints contain packages not installed或相似提示constraints 里写了依赖树中不存在的包从 constraints 移除该行或把它加入 requirements 主动安装安装成功但版本不符合预期约束文件路径错误、用错-r/-c参数检查加载方式修正路径本机能装、CI 不能装环境差异、pip 版本差异或平台 wheel 不同在多环境分别跑 dry-run统一 pip 版本3. 一步步排查需求与约束冲突3.1 准备干净现场先别急着改文件我的第一步永远是记录现场。先看python --version和pip --versionpip 版本太老时报错形式和解析策略都差很多升级到新版往往能输出更清晰的提示。然后新建一个干净的虚拟环境全程用里面的 pip 操作避免系统环境已有的包干扰解析结果。真正开始验证时用这个组合python -m venv /tmp/check_env /tmp/check_env/bin/pip install --upgrade pip /tmp/check_env/bin/pip install --dry-run --ignore-installed -r requirements.txt -c constraints.txt--dry-run只做解析不做落盘--ignore-installed让解析器当作全新环境计算这样能复现出真实冲突而不是被本地已安装包“救回来”。如果这条命令直接通过说明问题多半不是文件本身冲突而是环境干扰或安装顺序导致。3.2 用解析报告和依赖树把冲突点揪出来dry-run 报错之后不要急着猜。新版 pip 在ResolutionImpossible后面通常会附带一段The conflict is caused by:的冲突链说明把相互矛盾的依赖一条条列出来这就是读报错最快的线索。还可以再让 pip 输出一份 JSON 报告/tmp/check_env/bin/pip install --dry-run --ignore-installed \ -r requirements.txt -c constraints.txt \ --report /tmp/dep-report.json报告会写明解析阶段失败的是哪组包、哪条依赖。拿到包名之后再用 pipdeptree 看一眼依赖树/tmp/check_env/bin/pip install pipdeptree /tmp/check_env/bin/pipdeptree -r -p requests-p指定看某个包-r是反向依赖也就是“谁在依赖它”能帮你快速判断该动直接依赖还是该动约束。最后用pip index versions 包名查线上可用版本决定可调整的方向。需要提醒的是pip index在部分旧版 pip 里是实验性命令且依赖网络如果镜像源不支持也可以直接去商店页面查版本列表。3.3 一个真实修复案例照着做就能通假设项目里同时用到 requests 和 flask文件内容是这样# requirements.txt requests2.31.0 flask2.2.5 # constraints.txt urllib31.21 click8.0.0报错指向 requests 无法安装完整信息里能看到冲突链requests 2.31.0 要求urllib31.21.1,3约束却要求urllib31.21区间完全无交集。处理时先看约束是谁定的如果只是历史遗留的保守限制改成urllib33就能保留合理上限又不与 requests 冲突。如果约束来自安全公告比如 CVE 要求必须低于某个版本那就不能放宽约束需要把直接依赖 requests 升级到兼容新 urllib3 的版本同时把click8.0.0这类约束也一起校验一遍。改完再执行 dry-run直到无报错最后在干净环境里实际安装并跑一遍pip check确认整棵依赖树自洽。3.4 修改时怎么算清版本区间依赖解析本质就是区间求交集。直接依赖声明允许的版本范围和约束文件过滤后的范围必须存在共同值。比如 requests 给的是[1.21.1, 3)约束给的是[1.21, 1.21.1)这两个区间碰不到就会失败如果约束改成[1.21.1, 1.26)交集非空解析就能继续。实际操作里我一般优先放宽 constraints 的范围因为它是额外加的限制层是最容易松动的一层。如果 constraints 是外部安全审计的要求比如urllib31.27那就评估直接依赖能否升级到新版本而不是硬顶着约束走。记住一个原则提高约束范围而不降级约束范围放宽直接依赖时就选最新稳定版收缩时必须看依赖链上所有相关包的元数据。4. 让冲突不再天天上门几个防患手段4.1 把敏感的传递依赖转正很多冲突其实发生在传递依赖层面因为传递依赖的版本选择你看不见。你写了requests2.31.0真正安装的还有一堆它带上来的包这些包的版本完全由解析器决定。一个被多个包共用、版本又比较敏感的库比如 urllib3、charset-normalizer、typing-extensions 这类我的习惯是把它直接写进 requirements.txt显式声明版本。这样版本意图一眼就能看明白后续维护的人不会误删resolver 也会把它当作直接依赖优先满足避免它被某个传递依赖拖进奇怪的版本区间。代价是 requirements.txt 会比原来长几行但换来的是少踩几个小时的坑。4.2 用 pip-tools 把手动约束变成机器解析当项目进入多人维护阶段手动维护两个文件早晚会出事。我惯用的方案是 pip-toolspip install pip-tools # requirements.in 里只写顶层依赖 cat requirements.in flask2.2.5 requests2.31.0 # constraints.in 里写约束 cat constraints.in urllib31.27 # 生成 lock 文件 pip-compile requirements.in --constraint constraints.in --output-file requirements.lock.txtpip-compile 会把整棵依赖树解析进 requirements.lock.txt把传递依赖也全部锁死。以后改依赖重跑一次就能在编译阶段看到冲突完全不用等到 install 才炸。有个细节值得注意如果约束和直接依赖冲突pip-compile 会在输出里明确提示哪一个包与哪一行约束矛盾比 pip 的原始报错更友好。配合pip-sync使用时安装的就是和 lock 完全一致的集合环境漂移问题直接消失。4.3 在 CI 里加一道 dry-run 拦截比“会修冲突”更值钱的是“不让冲突流到部署前”。最简单的做法是在 CI 里挂一个解析门禁python -m venv /tmp/ci_venv /tmp/ci_venv/bin/pip install --dry-run --ignore-installed \ -r requirements.txt -c constraints.txt把这段脚本放在依赖文件变更的流水线里解析不过直接就 fail。成本非常低但能拦截掉大部分环境间不一致问题。很多团队直到生产部署才执行安装依赖冲突全在最后一步爆发到那时返工成本极高。让 dry-run 成为每次提交的例行检查比任何评论区的“我这边能跑”都可信得多。4.4 多环境复用时的一句话提醒最后提醒一句代码跨平台、跨 Python 版本复用时不要看到命令跑通就放行。同一个约束在 Python 3.8 和 Python 3.12 下的解析结果可能不同很多底层包会按 Python 版本发布不同的 wheel甚至不同系统上的依赖都不一样。我见过最典型的现场同事在 Windows 上装得好好的把 requirements.txt 和 constraints.txt 提交后CI 里的 Linux 环境直接报约束未满足原因是某个包为不同平台暴露了不同的依赖版本。稳妥做法是每个主用环境单独跑一次 dry-run环境差异大的项目就分别生成 lock 文件各环境用各自的锁。这套流程加上前几节说的排查手段基本能覆盖掉绝大多数依赖冲突问题。最后说点我自己的习惯。踩过几次坑之后我明白了一个道理真正难的不是修冲突而是搞清楚当初写下约束时的理由。现在我会在 constraints.txt 的每一行后面都写清原因比如# 安全公告要求暂不升级到指定版本或者# 测试框架兼容保持当前主版本。三个月后再回来维护看到注释就知道哪个约束能放松、哪个必须死守排查时间至少省一半。遇到ResolutionImpossible时我的反应已经很固定看报错、查依赖树、算区间、改一边、dry-run 验证。这套流程配合多环境先预检足以让绝大多数 Python 依赖管理问题在暴露之前就被摁住。