# How to Upgrade a Dependency Past Its Version Ceiling

A [pytest](https://pydevtools.com/handbook/reference/pytest.md) 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](https://pydevtools.com/handbook/reference/pyproject.toml.md), then force [uv](https://pydevtools.com/handbook/reference/uv.md) to re-resolve the package. Widening the bound alone won't do it, because uv keeps the version already in the [lockfile](https://pydevtools.com/handbook/explanation/what-is-a-lock-file.md) as long as it still fits.

## Confirm the ceiling is the blocker

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

```console
$ 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](https://pydevtools.com/handbook/explanation/what-is-a-version-specifier.md) 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](https://pydevtools.com/handbook/explanation/understanding-dependency-groups-in-uv.md), pass `--dev` so uv edits the right table:

```console
$ 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:

```console
$ 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:

```console
$ 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:

```console
$ 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:

```console
$ 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](https://github.com/astral-sh/uv/issues/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](https://pydevtools.com/handbook/how-to/how-to-maintain-a-uv-project.md). 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

- [Managing dependencies](https://docs.astral.sh/uv/concepts/projects/dependencies/) in the uv docs covers constraint syntax and the `--upgrade-package` flag.
- [How to use a uv lockfile for reproducible Python environments](https://pydevtools.com/handbook/how-to/how-to-use-a-uv-lockfile-for-reproducible-python-environments.md) explains what `uv.lock` records and why.
- [uv: A Complete Guide](https://pydevtools.com/handbook/explanation/uv-complete-guide.md) walks through the full project workflow.
