Skip to content

How to Upgrade a Dependency Past Its Version Ceiling

A pytest dependency pinned pytest>=8,<9 will not move to pytest 9, no matter how many times you run uv lock --upgrade. The upper bound forbids it, and uv lock --upgrade only resolves within the bounds you declare.

Crossing that ceiling takes two moves: raise the upper bound in pyproject.toml, then force uv to re-resolve the package. Widening the bound alone won’t do it, because uv keeps the version already in the lockfile as long as it still fits.

Confirm the ceiling is the blocker

Run the normal upgrade command and watch the capped package stay put:

$ uv lock --upgrade
Resolved 7 packages in 98ms

Nothing moves. uv lock --upgrade re-resolves every dependency to the newest version that fits the version specifiers in pyproject.toml, and <9 excludes every 9.x release. The constraint is doing exactly what it says.

Raise the ceiling and force the re-lock

One command rewrites the bound and re-resolves the package. Because pytest lives in the dev dependency group, pass --dev so uv edits the right table:

$ uv add --dev 'pytest>=8,<10' --upgrade-package pytest
Resolved 7 packages in 82ms
Prepared 1 package in 0.10ms
Uninstalled 1 package in 3ms
Installed 1 package in 3ms
 - pytest==8.4.2
 + pytest==9.1.1

The pytest>=8,<10 range rewrites the ceiling in pyproject.toml. The --upgrade-package pytest flag forces uv to re-resolve that package to the newest version the range now allows. uv add updates the lockfile and syncs the environment in the same step, so no separate uv sync is needed.

Tip

Quote the specifier. Every common shell (bash, zsh, PowerShell, cmd) reads bare < and > as redirection operators, so uv add pytest>=8,<10 without quotes writes stray files instead of adding the dependency.

For a package in the main [project.dependencies] list, drop --dev. Match the flag to the table the package lives in: --dev against a main dependency, or plain uv add against a group member, writes a duplicate entry to the other table and leaves the version unmoved.

Why widening the ceiling alone isn’t enough

Editing the ceiling by hand and running uv lock looks like it should work, but the locked version doesn’t budge:

$ uv lock
Resolved 7 packages in 2ms   # pytest stays at 8.4.2

uv keeps the version already recorded in the lockfile as long as it still satisfies the constraints, and 8.4.2 still fits >=8,<10. The uv docs put it plainly: “The locked version of the dependency will only change if necessary to satisfy the new constraints.” To move it, force a re-resolve of that one package, then install:

$ uv lock --upgrade-package pytest
Resolved 7 packages in 69ms
Updated pytest v8.4.2 -> v9.1.1
$ uv sync

That hand-edit cycle (raise the bound in pyproject.toml, uv lock --upgrade-package pytest, uv sync) is what the uv add one-liner collapses into a single command.

Jump straight to the new major

To drop the old major entirely, raise the floor above it. uv is then forced to move because the old version no longer satisfies the range:

$ uv add --dev 'pytest>=9,<10'
 - pytest==8.4.2
 + pytest==9.1.1

No --upgrade-package here: 8.4.2 fails >=9, so uv has to re-resolve to a 9.x release on its own.

Why not just run uv upgrade?

uv 0.11.27 ships a uv upgrade <package> subcommand, and it looks like the answer:

$ uv upgrade pytest
Resolved 7 packages in 71ms
Update pytest v8.4.2 -> v9.1.1
Updated requirement: `pytest>=8,<9` -> `pytest>=8,<10`

It rewrites the ceiling in pyproject.toml and prints an upgrade line, but in this release it stops there: uv.lock and the installed version stay at 8.4.2. The tracking issue (astral-sh/uv#6794) is still open. Until it lands fully, reach for uv add 'pkg>=8,<10' --upgrade-package pkg.

Keep upgrades flowing after this

Crossing a ceiling is a one-time edit per dependency. The recurring cadence (scheduled uv lock --upgrade, Dependabot PRs) belongs in how to maintain a uv project. Dependabot won’t relax a range constraint for you, which is exactly why this manual step exists: bump the ceiling once by hand, then let the automation keep the lock current inside the new range.

Learn More

Last updated on