The Problem of Dependabot's uv Ecosystem Breaking Lower Version Bounds in pyproject.toml and How to Deal with It

The Problem of Dependabot's uv Ecosystem Breaking Lower Version Bounds in pyproject.toml and How to Deal with It

Dependabot's uv ecosystem doesn't support versioning-strategy and always uses increase, which can rewrite pyproject.toml lower bounds to versions missing from PyPI and break CI. Here's how to prevent it with uv lock --check.
2026.07.18

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.

dependabot-uv-versioning-pitfall-flow

What Happened

The Problem

The lower bound of boto3 in pyproject.toml had been set to a version that doesn't exist on PyPI.

pyproject.toml
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

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.

dependabot.yml
- 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

ci.yml
- 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.

Share this article