The Problem of Dependabot's uv Ecosystem Breaking Lower Version Bounds in pyproject.toml and How to Deal with It
This page has been translated by machine translation. View original
Introduction
One day, CI suddenly failed with this error.
× No solution found when resolving dependencies:
╰─▶ Because only boto3<=1.43.48 is available and your project depends on
boto3>=1.43.49, we can conclude that your project's requirements are
unsatisfiable.
boto3>=1.43.49 — that version doesn't exist on PyPI. Who wrote this constraint?
The culprit was Dependabot.
This article summarizes what I learned about the mechanism by which Dependabot's uv ecosystem breaks the dependencies of a Python project, and the CI measures to prevent it.

What Happened
The Problem
The lower bound of boto3 in pyproject.toml had been set to a version that doesn't exist on PyPI.
dependencies = [
"boto3>=1.43.49", # ← This version does not exist
]
Identifying the Cause
I used git log to trace where this constraint was introduced.
git log -p --all -S "boto3" -- backend/pyproject.toml
It turned out that it had been rewritten from >=1.43.0 to >=1.43.49 in a PR created by Dependabot.
Dependabot's increase Strategy — Why Does It Raise the Lower Bound?
What Is versioning-strategy?
Dependabot has a setting called versioning-strategy that controls how dependency versions are updated.
| Strategy | Behavior |
|---|---|
increase |
Raises the lower bound to the latest resolved version |
increase-if-necessary |
Leaves it unchanged if the current constraint allows the new version |
lockfile-only |
Updates only the lockfile and doesn't touch the manifest |
widen |
Widens the range to include the new version (for libraries) |
Limitations of the uv Ecosystem
Here is the heart of the problem. Dependabot's uv ecosystem does not support the versioning-strategy setting (dependabot-core #12162). The increase strategy is always used, and the lower bound is rewritten to the latest resolved version.
In other words, even if you write boto3>=1.35, Dependabot will change it to boto3>=1.43.49 in the next PR.
>= vs ~= — Which Should You Use?
I wondered whether using ~= (compatible release) would solve this problem.
# >= : specify only a lower bound (no upper bound)
"boto3>=1.35"
# ~= : compatible release
"boto3~=1.43.0" # >= 1.43.0, < 1.44.0
>= Is Recommended for Applications
My research showed that >= is recommended for applications (things that are not libraries).
| Application | Library | |
|---|---|---|
| Purpose | Used only by your own project | Installed into other projects |
| Recommended specifier | >= (loose lower bound) |
>= or ~= |
| Version pinning | Pinned with uv.lock |
Left to the consumer's lockfile |
| Impact of Dependabot | Updating the lockfile is the main purpose | Updating the manifest is also meaningful |
In an application, the constraints in pyproject.toml merely indicate a lower bound meaning "anything older than this version won't work," and the versions actually installed are determined by uv.lock. Setting an upper bound with ~= would even block legitimate minor version updates.
And importantly: even if you use ~=, Dependabot rewrites the lower bound, so it is not a fundamental solution.
Switching to the pip Ecosystem Is Not a Solution
With Dependabot's pip ecosystem, you can use versioning-strategy: increase-if-necessary. With this, the lower bound is not changed when 1.43.48 comes out against a constraint of >=1.35.
- package-ecosystem: "pip"
versioning-strategy: increase-if-necessary
However, there is a big pitfall.
The pip ecosystem does not update uv.lock. This causes the opposite inconsistency: pyproject.toml gets updated but uv.lock stays outdated.
| Ecosystem | versioning-strategy |
Updates uv.lock |
|---|---|---|
uv |
Not supported (always increase) |
Yes |
pip |
Supports increase-if-necessary, etc. |
No |
package-ecosystem only specifies how Dependabot parses the manifest and doesn't affect the package manager you use locally. However, if it doesn't update the lockfile, there's no point in using it.
A Practical Measure: a CI Guard
Since we can't prevent the lower bound from being raised itself, the realistic measure is to prevent broken constraints from being merged.
Add uv lock --check to CI
- name: Install Python dependencies
run: cd backend && uv sync --group dev
- name: Verify dependency constraints are satisfiable
run: cd backend && uv lock --check
- name: Lint (ESLint + ruff)
run: pnpm run lint
uv lock --check verifies whether the constraints in pyproject.toml are consistent with the contents of uv.lock. If the constraints are unsatisfiable, this step fails with a clear error message.
uv sync also fails in the same case, but uv lock --check is better in that:
- The error message is clearer: it produces an error specific to dependency resolution
- It runs faster: it skips installing packages
- The intent is clear: you can tell what is being checked just from the CI step name
Be Careful When Combining with auto-merge
If you have Dependabot auto-merge configured, make sure the CI check is set as a required status check. gh pr merge --auto waits until the required checks pass, but if no required checks are configured, it merges immediately.
Summary
| Lesson | Details |
|---|---|
Dependabot's uv ecosystem doesn't support versioning-strategy |
increase is always used, and the lower bound is raised |
Switching to the pip ecosystem is not recommended |
Because it doesn't update uv.lock |
Using ~= or loosening the lower bound is only temporary |
It reverts in the next Dependabot PR |
Adding uv lock --check to CI is the practical solution |
Prevents unsatisfiable constraints from being merged |
| Set required status checks for auto-merge | Prevents merging without passing CI |
Until versioning-strategy is supported in Dependabot's uv ecosystem (#12162), a CI guard is the most practical defense. Dependabot is a handy tool, but if you don't understand its behavior correctly, it can end up breaking your builds.