diff --git a/docs/reference/internals/resolver.md b/docs/reference/internals/resolver.md index b4acdab34..9dcd21c81 100644 --- a/docs/reference/internals/resolver.md +++ b/docs/reference/internals/resolver.md @@ -156,26 +156,62 @@ Windows, Linux and macOS. To ensure that a resolution with `requires-python = ">=3.9"` can actually be installed for the included Python versions, uv requires that all dependencies have the same minimum Python version. Package versions that declare a higher minimum Python version, e.g., `requires-python = ">=3.10"`, -are rejected, because a resolution with that version can't be installed on Python 3.9. For -simplicity and forward compatibility, only lower bounds in `requires-python` are respected. For -example, if a package declares `requires-python = ">=3.8,<4"`, the `<4` marker is not propagated to -the entire resolution. +are rejected, because a resolution with that version can't be installed on Python 3.9. This ensures +that when you are on an old Python version, you can install old packages, instead of getting newer +packages that require newer Python syntax or standard library features. -This default is a problem for packages that use the version-dependent C API of CPython, such as -numpy. Each numpy release support 4 Python minor versions, e.g., numpy 2.0.0 has wheels for CPython -3.9 through 3.12 and declares `requires-python = ">=3.9"`, while numpy 2.1.0 has wheels for CPython -3.10 through 3.13 and declares `requires-python = ">=3.10"`. The means that when we resolve a -`numpy>=2,<3` requirement in a project with `requires-python = ">=3.9"`, we resolve numpy 2.0.0 and -the lockfile doesn't install on Python 3.13 or newer. To alleviate this, whenever we reject a -version due to a too high Python requirement, we fork on that Python version. This behavior is -controlled by `--fork-strategy`. In the example case, upon encountering numpy 2.1.0 we fork into -Python versions `>=3.9,<3.10` and `>=3.10` and resolve two different numpy versions: +uv ignores upper-bounds on `requires-python`, with special handling for packages with only +ABI-specific wheels. For example, if a package declares `requires-python = ">=3.8,<4"`, the `<4` +part is ignored. There is a detailed discussion with drawbacks and alternatives in +[#4022](https://github.com/astral-sh/uv/issues/4022) and this +[DPO thread](https://discuss.python.org/t/requires-python-upper-limits/12663), this section +summarizes the aspects most relevant to uv's design. + +For most projects, it's not possible to determine whether they will be compatible with a new version +before it's released, so blocking newer versions in advance would block users from upgrading or +testing newer Python versions. The exceptions are packages which use the unstable C ABI or internals +of CPython such as its bytecode format. + +Introducing a `requires-python` upper bound to a project that previously wasn't using one will not +prevent the project from being used on a too recent Python version. Instead of failing, the resolver +will pick an older version without the bound, circumventing the bound. + +For the resolution to be as universally installable as possible, uv ensures that the selected +dependency versions are compatible with the `requires-python` range of the project. For example, for +a project with `requires-python = ">=3.12"`, uv will not use a dependency version with +`requires-python = ">=3.13"`, as otherwise the resolution is not installable on Python 3.12, which +the project declares to support. Applying the same logic to upper bounds means that bumping the +upper Python version bound on a project makes it compatible with less dependency versions, +potentially failing to resolve when no version of a dependency supports the required range. (Bumping +the lower Python version bound has the inverse effect, it only increases the set of supported +dependency versions.) + +Note that this is different for Conda, as the Conda solver also determines the Python version, so it +can choose a lower Python version instead. Conda can also change metadata after a release, so it can +update compatibility for a new Python version, while metadata on PyPI cannot be changed once +published. + +Ignoring an upper bound is a problem for packages such as numpy which use the version-dependent C +API of CPython. As of writing, each numpy release support 4 Python minor versions, e.g., numpy 2.0.0 +has wheels for CPython 3.9 through 3.12 and declares `requires-python = ">=3.9"`, while numpy 2.1.0 +has wheels for CPython 3.10 through 3.13 and declares `requires-python = ">=3.10"`. The means that +when uv resolves a `numpy>=2,<3` requirement in a project with `requires-python = ">=3.9"`, it +selects numpy 2.0.0 and the lockfile doesn't install on Python 3.13 or newer. To alleviate this, +whenever uv rejects a version that requires a newer Python version, we fork by splitting the +resolution markers on that Python version. This behavior can be controlled by `--fork-strategy`. In +the example case, upon encountering numpy 2.1.0 we fork into Python versions `>=3.9,<3.10` and +`>=3.10` and resolve two different numpy versions: ``` numpy==2.0.0; python_version >= "3.9" and python_version < "3.10" numpy==2.1.0; python_version >= "3.10" ``` +There's one case where uv does consider the upper bound: When the project uses an upper bound on +requires Python, such as `requires-python = "==3.13.*"` for an application that only deploys to +Python 3.13. uv prunes wheels from the lockfile that are outside the range (e.g., `cp312` and +`cp314`) in a post-processing step, which does not influence the resolution itself. + ## URL dependencies In uv, a dependency can either be a registry dependency, a package with a version specifier or the