PEP: 817
Title: Wheel Variants: Beyond Platform Tags
Author: Jonathan Dekhtiar <jonathan@dekhtiar.com>,
        Michał Górny <mgorny@quansight.com>,
        Konstantin Schütze <konstin@mailbox.org>,
        Ralf Gommers <ralf.gommers@gmail.com>,
        Andrey Talman <atalman@meta.com>,
        Charlie Marsh <charlie@astral.sh>,
        Michael Sarahan <msarahan@gmail.com>,
        Eli Uriegas <eliuriegas@meta.com>,
        Barry Warsaw <barry@python.org>,
        Donald Stufft <donald@stufft.io>,
        Andy R. Terrel <andy.terrel@gmail.com>
Discussions-To: https://discuss.python.org/t/pep-817-wheel-variants-beyond-platform-tags/105860
Status: Draft
Type: Standards Track
Topic: Packaging
Created: 10-Dec-2025
Post-History: `24-Jan-2026 <https://discuss.python.org/t/pep-817-wheel-variants-beyond-platform-tags/105860>`__


Abstract
========

Python's existing wheel packaging format uses
:doc:`packaging:specifications/platform-compatibility-tags` to specify a
given wheel's supported environments. These tags are unable to express
modern hardware configurations and their features, such as the
availability of GPU acceleration. The tags fail to provide custom
package variants, such as builds against different dependency ABIs.
These inabilities are particularly challenging for scientific computing,
artificial intelligence (AI), machine learning (ML), and
high-performance computing (HPC) communities.

This PEP proposes "Wheel Variants", an extension to the
:doc:`packaging:specifications/binary-distribution-format`. This
extension introduces a mechanism for package maintainers to declare
multiple build variants for the same package version, while allowing
installers to automatically select the most appropriate variant based on
system hardware and software characteristics. More specifically, it
proposes:

- An evolution of the wheel format called **Wheel Variant** that allows
  wheels to be distinguished by hardware or software attributes.

- A **variant provider plugin** interface that allows installers to
  dynamically detect platform attributes and select the most suitable
  wheel.

The goal is for the obvious installation commands (``{tool} install
<package>``) to select the most appropriate wheel, and provide the best
user experience.


Motivation
==========

The `2024 Python Developers Survey`__ shows that a significant
proportion of Python's users have scientific computing use-cases. This
includes data analysis (40% of respondents), machine learning (30%), and
data engineering (30%). Many of the software packages developed for
these areas rely on diverse hardware features that cannot be adequately
expressed in the current wheel format, as highlighted in `the
limitations of platform compatibility tags`_.

__ https://lp.jetbrains.com/python-developers-survey-2024/#purposes-for-using-python

For example, packages such as PyTorch_ need to
be built for specific CUDA or ROCm versions, and that information cannot
currently be included in the wheel tag. Having to build multiple wheels
targeting very different hardware configurations forces maintainers into
various distribution strategies that are suboptimal, and create friction
for users and authors of other software who wish to depend on the
package in question.

A few existing approaches are explored in `Current workarounds and their
drawbacks`_. They include maintaining separate package indexes for
different hardware configurations, bundling all potential variants into
a single wheel of considerable size, or using separate package names
(``mypackage-gpu``, ``mypackage-cpu``, etc.). Each of these approaches
has significant drawbacks and potential security implications.

The limitations of platform compatibility tags
----------------------------------------------

The current wheel format encodes compatibility through three platform
tags:

1. **Python tag**: encoding the minimum Python version and optionally
   restricting Python distributions (e.g., ``py3`` for any Python 3,
   ``py313`` for Python 3.13 or newer, ``cp313`` for specifically
   CPython, 3.13 or newer).
2. **ABI tag**: encoding the Python ABI required by any extension
   modules (e.g., ``none`` for no requirement, ``abi3`` for the CPython
   stable ABI, ``cp313`` for extensions requiring CPython 3.13 ABI).
3. **Platform tag**: currently encoding the operating system,
   architecture and core system libraries (e.g., ``any`` for any
   platform, ``manylinux_2_34_x86_64`` for x86-64 Linux system with
   glibc 2.34 or newer, ``macosx_14_0_arm64`` for arm64 macOS 14.0
   or newer system.

These tags are limited to expressing the most fundamental properties
of the Python interpreter, operating system and the broad CPU
architectures. They cannot express anything more detailed, including
non-CPU hardware requirements or library ABI constraints.

This lack of flexibility has led many projects to find sub-optimal - yet
necessary - workarounds, such as the manual installation command
selector provided by the PyTorch team. This complexity represents a
fundamental scalability issue with the current tag system that is not
extensible enough to handle the combinatorial complexity of build
options.


Current workarounds and their drawbacks
---------------------------------------

Runtime CPU dispatching
'''''''''''''''''''''''

Projects such as NumPy_ currently resort to building wheels for a
baseline CPU target, and using runtime dispatching for
performance-critical routines. Such a solution requires additional
effort from package maintainers, and usually doesn't let the code
benefit from compiler optimizations outside the few select functions.

For comparison, building GROMACS_ for
higher CPU baselines proved to provide significant speedups:

    .. figure:: pep-0817/avx512_gromacs_benchmark.svg
        :alt: A bar graph comparing GROMACS performance (in ns/day) with
              various targets. The first two bars are labeled "yum
              (2018.8)" and "generic (SSE2)", reach about 1.0 ns/day and
              are both marked as "SSE2". The next bar is labeled
              "ivybridge" ("AVX") and reaches almost 1.5 ns/day. Two
              following bars are labeled "haswell" and "broadwell" (both
              "AVX2") and exceed 1.5 ns/day slightly. The last two bars
              are labeled "skylake_avx512" and "cascadelake" (both
              "AVX512") and reach almost 2.0 ns/day.

        Performance of GROMACS 2020.1 built for different generations of
        CPUs. Vertical axis shows performance expressed in ns/day, a
        GROMACS-specific measure of simulation speed (higher is better).

    Compiling GROMACS_ for architectures that can exploit the AVX-512
    instructions supported by the Intel Cascade Lake microarchitecture
    gives an additional 18% performance improvement relative to using
    AVX2 instructions, with a speedup of about 70% compared to a generic
    GROMACS installation with only SSE2.

    — `archspec: A library for detecting, labeling, and reasoning about
    microarchitectures
    <https://tgamblin.github.io/pubs/archspec-canopie-hpc-2020.pdf>`__


Separate package indexes as variants
''''''''''''''''''''''''''''''''''''

Projects such as `PyTorch <https://pytorch.org/get-started/locally/>`__
and `RAPIDS <https://docs.rapids.ai/install/#selector>`__
currently distribute packages that approximate "variants" through
separate package indexes with custom URLs. We will use the
example of PyTorch, while the problem, the workarounds, and the impact
on users also apply to other packages.

.. figure:: pep-0817/pytorch_variant_selector.png
    :class: invert-in-dark-mode
    :alt: A grid-based selector for PyTorch versions. Individual rows
          provide the choice of PyTorch Build (stable or nightly),
          operating system (Linux, Mac, Windows), package (Pip,
          LibTorch, Source), language (Python, C++ / Java), and Compute
          Platform (CUDA 12.6, CUDA 12.8, CUDA 13.0, ROCM 6.4, CPU).
          Below these rows, the pip install command for the selected
          variant is provided, utilizing the --index-url parameter.

    The PyTorch install selector
    (https://pytorch.org/get-started/locally/, captured 22-Aug-2025)

PyTorch uses a combination of index URLs per accelerator type and local
version segments as accelerator tag (such as ``+cu130``, ``+rocm6.4`` or
``+cpu``) . Users need to first determine the correct index URL for
their system, and add an index specifically for PyTorch.

.. code:: bash

    pip install torch --index-url https://download.pytorch.org/whl/cu129

Tools need to implement special handling for the way PyTorch uses local
version segments. These requirements break the pattern that packages
are usually installed with. Problems with installing PyTorch
are a very common point of user confusion. To quantify this, on
2025-12-05, 552 out of 8136 (6.8%), of issues on `uv's issue tracker
<https://github.com/astral-sh/uv/issues/>`__ contained the term "torch".

**Security Risk:** This approach has unfortunately led to supply
chain attacks - more details on the `PyTorch Blog
<https://pytorch.org/blog/compromised-nightly-dependency/>`__. It's a
non-trivial problem to address which has forced the PyTorch team to
create a complete mirror of all their dependencies, and is one of the
core motivations behind :pep:`766`.

The complexity of configuration often leads to projects providing ad-hoc
installation instructions that do not provide for seamless package
upgrades.


Package names as variants
'''''''''''''''''''''''''

Packages such as `XGBoost
<https://xgboost.readthedocs.io/en/stable/install.html>`__ use different
package names to approximate variants:

.. code:: bash

    pip install xgboost      # NVIDIA GPU variant
    pip install xgboost-cpu  # CPU-only variant

Maintainers of other software cannot express that they depend on either
of the available variants being selected. They need to
either depend on a specific variant, provide multiple alternative
dependency sets using extras, or even publish their own software using
multiple package names matching upstream variants.

Commonly, these packages install overlapping files. Since Python
packaging does not support expressing that two packages are mutually
exclusive, installers can install both of them to the same environment,
with the package installed second overwriting files from the one
installed first. This leads to runtime errors, and
the possibility of incidentally switching between variants depending on
the way package upgrades are ordered.

An additional limitation of this approach is that publishing a new
release synchronously across multiple package names is not currently
possible. :pep:`694` proposes adding such a mechanism for multiple
wheels within a single package, but extending it to multiple packages is
not a goal.

**Security Risk:** proliferation of suffixed variant packages
leads users to expect these suffixes in other packages, making name
squatting much easier. For example, one could create a malicious
``numpy-cuda`` package that users will be lead to believe it's a CUDA
variant of NumPy.

As of the time of writing, CuPy_ has already registered a total of 55
``cupy*`` packages with different names, most of them never actually
used (they are only visible through the use of Simple API), and a large
part of the remaining ones no longer updated. This clearly highlights
the magnitude of the problem, and the effort put into countering the
risk of name squatting.

.. code:: text

    cupy
    cupy-cuda70 cupy-cuda75 cupy-cuda80 cupy-cuda90 cupy-cuda91
    cupy-cuda92 cupy-cuda100 cupy-cuda101 cupy-cuda102
    cupy-cuda110 cupy-cuda111 cupy-cuda112 cupy-cuda113 cupy-cuda114
    cupy-cuda115 cupy-cuda116 cupy-cuda117 cupy-cuda118 cupy-cuda119
    cupy-cuda11x
    cupy-cuda120 cupy-cuda121 cupy-cuda122 cupy-cuda123 cupy-cuda124
    cupy-cuda125 cupy-cuda126 cupy-cuda127 cupy-cuda128 cupy-cuda129
    cupy-cuda12x
    cupy-cuda13x
    cupy-rocm-4-0 cupy-rocm-4-1 cupy-rocm-4-2 cupy-rocm-4-3
    cupy-rocm-4-4 cupy-rocm-4-5 cupy-rocm-5-0 cupy-rocm-5-1
    cupy-rocm-5-2 cupy-rocm-5-3 cupy-rocm-5-4 cupy-rocm-5-5
    cupy-rocm-5-6 cupy-rocm-5-7 cupy-rocm-5-8 cupy-rocm-5-9
    cupy-rocm-6-0 cupy-rocm-6-1 cupy-rocm-6-2 cupy-rocm-6-3
    cupy-rocm-7-0 cupy-rocm-7-1


Package extras as variants
''''''''''''''''''''''''''

`JAX <https://docs.jax.dev/en/latest/installation.html>`__ uses a
plugin-based approach. The central ``jax`` package provides a number of
extras that can be used to install additional plugins,
e.g. ``jax[cuda12]`` or ``jax[tpu]``. This is far from ideal as
``pip install jax`` (with no extra) leads to a nonfunctional
installation, and consequently dependency chains, a fundamental expected
behavior in the Python ecosystem, are dysfunctional.

JAX includes 12 extras to cover all use cases - many of which
overlap and could be misleading to users if they don't read the
documentation in detail. Most of them are technically mutually
exclusive, though it is currently impossible to correctly express this
within the package metadata.

.. code:: email

    Provides-Extra: minimum-jaxlib
    Provides-Extra: cpu
    Provides-Extra: ci
    Provides-Extra: tpu
    Provides-Extra: cuda
    Provides-Extra: cuda12
    Provides-Extra: cuda13
    Provides-Extra: cuda12-local
    Provides-Extra: cuda13-local
    Provides-Extra: rocm
    Provides-Extra: k8s
    Provides-Extra: xprof


Bundled universal packages - monolithic builds
''''''''''''''''''''''''''''''''''''''''''''''

Including all possible variants in a single wheel is another option, but
this leads to excessively large artifacts, wasting bandwidth and leading
to slower installation times for users who only need one specific
variant. In some cases, such artifacts cannot be hosted on PyPI because
they exceed its size limits.


Wheel variant selection via source distribution
'''''''''''''''''''''''''''''''''''''''''''''''

`FlashAttention <https://github.com/Dao-AILab/flash-attention>`__ does
not publish wheels on PyPI at all, but instead publishes a customized
source distribution that performs platform detection, downloads the
appropriate wheel from an upstream server, and then provides it to the
installer. This approach can select the optimal variant automatically,
but it prevents binary-only installs from working, requires a slow and
error-prone build via a source distribution, and breaks common caching
assumptions tied to the wheel filename. It also requires a specially
prepared build environment that contains the ``torch`` package matching
the version that the software will run against, which requires building
without build isolation. On the project side, it requires hosting wheels
separately.

**Security Risk:** Similar to regular source builds, this
model requires running arbitrary code at install time. The wheels
are downloaded entirely outside the package manager's control, extending
the attack surface to two separate wheel download implementations and
preventing proper provenance tracking.


Ecosystem fragmentation
'''''''''''''''''''''''

The lack of standardized support for solving against hardware
and ABI requirements has led to ecosystem fragmentation:

* **Inconsistent User Experience**: Each project uses different
  installation methods, creating confusion and reducing discoverability.

* **Development Tool Complications**: Installers, IDEs, and CI/CD
  systems struggle to handle non-standard installation requirements.

* **Fragility**: The established workarounds are often error-prone,
  and in the past they have lead to issues such as downloading incorrect
  artifacts.


Impact on scientific computing and AI/ML workflows
--------------------------------------------------

The packaging limitations particularly affect scientific computing and
AI/ML applications where performance optimization is critical:

    The current wheel format's lack of hardware awareness creates a
    suboptimal experience for hardware-dependent packages. While plugins
    help with smaller and well scoped packages, users must currently
    manually identify the correct variant (e.g., ``jax[cuda13]``) to
    avoid generic defaults or incompatible combinations. We need a
    system where ``pip install jax`` automatically selects packages
    matching the user's hardware, unless explicitly overridden.

    Wheel variants are a clear step in the right direction in this
    regard.

    — Michael Hudgins, JAX_ Developer Infrastructure Lead

They affect everyone from package authors to end users of all skill
levels, including students, scientists and engineers:

    Accessing compute to run models and process large datasets has been
    a pain point in scientific computing for over a decade. Today,
    researchers and data scientists still spend hours to days installing
    core tools like PyTorch before they can begin their work. This
    complexity is a significant barrier to entry for users who want to
    use Python in their daily work. The WheelNext Wheel Variants
    proposal offers a pathway to address persistent installation and
    compute-access problems within the broader packaging ecosystem
    without creating another, new and separate solution. Let's focus on
    the big picture of enhancing user experience - it will make a real
    difference.

    — Leah Wasser, Executive Director and Founder of `pyOpenSci
    <https://www.pyopensci.org/>`__


Heterogeneous computing environments
''''''''''''''''''''''''''''''''''''

Research institutions and cloud providers manage heterogeneous
computing clusters with different architectures (CPU, Hardware
accelerators, ASICS, etc.). The current system requires
environment-specific installation procedures, making reproducible
deployment difficult. This situation also contributes to making
"scientific papers" difficult to reproduce. Application authors focused
on improving that are hindered by the packaging hurdles too:

    We've been developing a package manager for Spyder, a Python IDE for
    scientists, engineers and data analysts, with three main aims.
    First, to make our users' life easier by allowing them to create
    environments and install packages using a GUI instead of introducing
    arcane commands in a terminal. Second, to make their research code
    reproducible, so they can share it and its dependencies with their
    peers. And third, to allow users to transfer their code to machines
    in HPC clusters or the cloud with no hassle, so they can leverage
    the vast compute resources available there. With the improvements
    proposed by this PEP, we'd be able to make that a reality for all
    PyPI users because installing widely used scientific libraries (like
    PyTorch and CuPy) for the right GPU and instruction set and would be
    straightforward and transparent for tools built on top of uv/pip.

    — Carlos Córdoba, lead developer of the `Spyder IDE`_


Artificial intelligence, machine learning, and deep learning
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

The recent advances in modern AI workflows increasingly rely on GPU
acceleration, but the current packaging system makes deployment complex
and adds a significant burden on open source developers of the entire
tool stack (from build backends to installers, not forgetting the
package maintainers).

    PyTorch's extensive wheel support was always state of the art and
    provided hardware accelerator support from day zero via our `package
    selector <https://pytorch.org/get-started/locally/>`__. We believe
    this was always a superpower of PyTorch to get things working out of
    the box for our users. Unfortunately, the infrastructure supporting
    these is very complex, hard to maintain and inefficient (for us, our
    users and package repositories).

    With the number of hardware we support growing rapidly again, we are
    very supportive of the wheel variants efforts that will allow us to
    get PyTorch install instructions to be what our users have been
    expecting since PyTorch was first released: ``pip install torch``

    — The PyTorch_ Core Maintainers

The lead maintainer of XGBoost_ enumerates a
number of problems XGBoost has that he expects will be addressed by
wheel variants:

    * Large download size, due to the use of "fat binaries" for multiple
      SMs [GPU targets]. Currently, XGBoost builds for 11 different SMs.

    * The need for a separate packaging name for CPU-only package.
      Currently we ship a separate package named ``xgboost-cpu``,
      requiring users to maintain separate ``requirements.txt`` files.
      See `xgboost#11632
      <https://github.com/dmlc/xgboost/issues/11632>`__ for an example.

    * Complex dispatching logic for multiple CUDA versions. Some
      features of XGBoost require new CUDA versions (12.5 or 12.8),
      while the XGBoost wheel targets 12.0. As a result, we maintain a
      fairly complex dispatching logic to detect CUDA and driver
      versions at runtime. Such dispatching logic should be best
      implemented in a dedicated piece of software like the NVIDIA
      provider plugin, so that the XGBoost project can focus on its core
      mission.

    * Undefined behavior due to presence of multiple OpenMP runtimes.
      XGBoost is installed in a variety of systems with different OpenMP
      runtimes (or none at all). So far, XGBoost has been vendoring a
      copy of OpenMP runtime, but this is increasingly untenable. Users
      get undefined behavior such as crashes or hangs when multiple
      incompatible versions of OpenMP runtimes are present in the
      system. (This problem was particularly bad on MacOS, so much so
      that the MacOS wheel for XGBoost no longer bundles OpenMP.)

    — Philip Hyunsu Cho, a lead maintainer of XGBoost_

The complexity of packaging is distracting developers from focusing on
the actual goals for their software:

    We maintain a scientific software tool that uses deep learning for
    analyzing biological motion in image sequences that has gotten
    traction (>35k users, >80 countries) due to its user friendliness as
    a frontend for training custom models on specialized scientific
    data. Our userbase are scientists who spend all day doing brain
    surgeries and molecular genetics to discover cures to diseases. It
    is entirely unreasonable to expect that they should have to learn
    about hardware accelerator driver compatibility matrices,
    environment managers, and keep up with the ever changing Python
    packaging ecosystem just to be able to analyze their data.

    In recognition of this, my team has spent an inordinate amount of
    time on maintaining dependencies and packaging hacks to ensure that
    our tool, which now undergirds the reproducibility of millions of
    dollars worth of research studies, remains compatible with every
    platform. In the past couple of years, we estimate that we've spent
    hundreds of hours and over $250,000 of taxpayer-supported research
    funding engineering solutions to this problem. WheelNext would have
    solved this entirely, allowing us to focus our efforts on
    understanding and treating neurodegenerative diseases.

    — Talmo Pereira, Ph.D., author of SLEAP_ and Principal Investigator
    at the Salk Institute for Biological Studies

The potential for improvement can be summarized as:

    This PEP is a significant step forward in improving the deployment
    challenges of the Python ecosystem in the face of increasingly
    complex and varied hardware configurations. By enabling multiple
    deployment targets for the same libraries in a standard way, it will
    consolidate and simplify many awkward and time-consuming
    work-arounds developers have been pursuing to support the rapidly
    growing AI/ML and scientific computing worlds.

    — Travis Oliphant, the author of NumPy_ and SciPy_ and Chief AI
    Architect at OpenTeams


Out-of-scope features
---------------------

This PEP presents the minimal scope required to meet modern heterogenous
system needs. It leaves aspects beyond the minimal scope to evolve via
tools or future PEPs. A non-exhaustive list of these aspects include:

- The format of a static file to select variants deterministically or
  include variants in a ``pylock.toml`` file,
- The list of variant providers that are vendored or re-implemented by
  installers,
- The specific opt-in mechanisms and UX for allowing an installer to run
  non-vendored variant providers,
- How to instruct build backends to emit variants through the :pep:`517`
  mechanism.


Prior art
---------

This problem is not unique to the Python ecosystem, different groups and
ecosystems have come up with various answers to that very problem. This
section will focus on highlighting the strengths and weaknesses of the
different approaches taken by various communities.


Conda - conda-forge
'''''''''''''''''''

`Conda <https://docs.conda.io>`__ is a binary-only package ecosystem
that uses aggregated metadata indexes for resolution rather than
filename parsing. Unlike the
:doc:`packaging:specifications/simple-repository-api`, conda's
resolution relies on `repodata indexes per platform
<https://docs.conda.io/projects/conda-build/en/stable/concepts/generating-index.html>`__
containing full metadata, making filenames purely identifiers with no
parsing requirements.

**Variant System**: In `2016-2017
<https://www.anaconda.com/blog/package-better-conda-build-3>`__,
conda-build introduced variants to differentiate packages with identical
name/version but different dependencies.

.. code:: bash

    pytorch-2.8.0-cpu_mkl_py313_he1d8d61_100.conda      # CPU + MKL variant
    pytorch-2.8.0-cuda128_mkl_py313_hf206996_300.conda  # CUDA 12.8 + MKL variant
    pytorch-2.8.0-cuda129_mkl_py313_he100a2c_300.conda  # CUDA 12.9 + MKL variant

A hash (computed from variant metadata) prevents filename collisions;
actual variant selection happens via standard dependency constraints in
the solver. No special metadata parsing is needed—installers simply
resolve dependencies like:

.. code:: bash

    conda install pytorch mkl

**Mutex Metapackages**: Python metadata and conda metadata do not have
good ways to express ideas like "this package conflicts with that one."
The main mechanism for enforcement is sharing a common package name -
only one package with a given name can exist at one time. Mutex
metapackages are sets of packages with the same name, but different
build string. Packages depend on specific mutex builds (e.g.,
``blas=*=openblas`` vs ``blas=*=mkl``) to avoid problems with related
packages using different dependency libraries, such as NumPy_ using
`OpenBLAS <https://www.openmathlib.org/OpenBLAS/>`__ and SciPy_ using
`MKL
<https://www.intel.com/content/www/us/en/developer/tools/oneapi/onemkl.html>`__.

**Example software variants**:
`BLAS <https://conda-forge.org/docs/maintainer/knowledge_base/#blas>`__,
`MPI
<https://conda-forge.org/docs/maintainer/knowledge_base/#message-passing-interface-mpi>`__,
`OpenMP
<https://conda-forge.org/docs/maintainer/knowledge_base/#openmp>`__,
`noarch vs native
<https://conda-forge.org/blog/2024/10/15/python-noarch-variants/>`__

**Virtual Packages**: `Introduced in 2019
<https://github.com/conda/conda/pull/8267>`__, virtual packages inject
system detection (CUDA version, glibc, CPU features) as solver
constraints. Built packages express dependencies like ``__cuda >=12.8``,
and the installer verifies compatibility at install time. Current
virtual packages include ``archspec`` (CPU capabilities), OS/system
libraries, and CUDA driver version. Detection logic is tool-specific
(`rattler
<https://github.com/conda/rattler/tree/main/crates/rattler_virtual_packages/src>`__,
`mamba
<https://github.com/mamba-org/mamba/blob/main/libmamba/src/core/virtual_packages.cpp>`__).


Spack / Archspec
''''''''''''''''

`archspec <https://github.com/archspec/archspec>`__ is a library for
detecting, labeling, and reasoning about CPU microarchitecture variants,
developed for the `Spack <https://spack.io/>`__ package manager.

**Variant Model:** CPU Microarchitectures (e.g., ``haswell``,
``skylake``, ``zen2``, ``armv8.1a``) form a `Directed Acyclic Graph
(DAG) encoding binary compatibility
<https://tgamblin.github.io/pubs/archspec-canopie-hpc-2020.pdf>`__,
which helps at resolve to express that ``packageB`` depends on
``packageA``. The ordering is partial because (1) separate ISA families
are incomparable, and (2) contemporary designs may have incompatible
feature sets—cascadelake and cannonlake are incomparable despite both
descending from skylake, as each has unique AVX-512 extensions.

**Implementation:** A language-agnostic JSON database stores
microarchitecture metadata (features, compatibility relationships,
compiler-specific optimization flags). Language bindings provide
detection (queries ``/proc/cpuinfo``, matches to microarchitecture with
largest compatible feature subset) and compatibility comparison
operators.

**Package Manager Integration:** Spack records target microarchitecture
as package provenance (``spack install fftw target=broadwell``),
automatically selects compiler flags, and enables
microarchitecture-aware binary caching. The `European Environment for
Scientific Software Installations (EESSI)
<https://onlinelibrary.wiley.com/doi/full/10.1002/spe.3075>`__
distributes optimized builds in separate subdirectories per
microarchitecture (e.g., ``x86_64``, ``armv8.1a``, ``haswell``);
runtime initialization uses ``archspec`` to select best compatible build
when no exact match exists.


Gentoo Linux
''''''''''''

`Gentoo Linux <https://www.gentoo.org>`__ is a source-first distribution
with support for extensive package customization. This is primarily
achieved via `USE flags
<https://devmanual.gentoo.org/general-concepts/use-flags/index.html>`__:
boolean flags exposed by individual packages and permitting fine-tuning
the enabled features, optional dependencies and some build parameters
(e.g. ``jpegxl`` for JPEG XL image format support,
``cpu_flags_x86_avx2`` for AVX2 instruction set use). Flags can be
toggled individually, and separate binary packages can be built for
different sets of flags. The package manager can either pick a binary
package with matching configuration or build from source.

API and ABI matching is primarily done through use of `slotting
<https://devmanual.gentoo.org/general-concepts/slotting/index.html>`__.
Slots are generally used to provide multiple versions or variants of
given package that can be installed alongside (e.g. different major GTK+
or LLVM versions, or GTK+3 and GTK4 builds of WebKitGTK), whereas
subslots are used to group versions within a slot, usually corresponding
to the library ABI version. Packages can then declare dependencies bound
to the slot and subslot used at build time. Again, separate binary
packages can be built against different dependency slots. When
installing a dependency version falling into a different slot or
subslot, the package manager may either replace the package needing that
dependency with a binary packages built against the new slot, or rebuild
it from source.

Normally, the use of slots assumes that upgrading to the newest version
possible is desirable. When more fine-grained control is desired, slots
are used in conjunction with USE flags. For example,
``llvm_slot_{major}`` flags are used to select a LLVM major version to
build against.


Overview and rationale
======================

Wheel variant glossary
----------------------

Variant Wheels
    Wheels that share the same distribution name, version, build number,
    and platform compatibility tags, but are distinctly identified by an
    arbitrary set of variant properties.

Variant Namespace
    An identifier used to group related features provided by a single
    provider (e.g., ``nvidia``, ``x86_64``, ``arm``, etc.).

Variant Feature
    A specific characteristic (key) within a namespace (e.g.,
    ``version``, ``avx512_bf16``, etc.) that can have one or more
    values.

Variant Property
    A 3-tuple (``namespace :: feature-name :: feature-value``)
    describing a single specific feature and its value. If a feature has
    multiple values, each is represented by a separate property.

Variant Label
    A string (up to 16 characters) added to the wheel filename to
    uniquely identify variants.

Null Variant
    A special variant with zero variant properties and the reserved
    label ``null``. Always considered supported but has the lowest
    priority among wheel variants, while being preferably chosen over
    non-variant wheels.

Variant Provider
    A provider of supported and valid variant properties for a specific
    namespace, usually in the form of a Python package that implements
    system detection.

Install-time Provider
    A provider implemented as a plugin that can be queried during wheel
    installation.

Ahead-of-Time Provider
    A provider that features a static list of supported properties which
    is then embedded in the wheel metadata. Such a list can either be
    embedded in ``pyproject.toml`` or provided by a plugin queried at
    build time.


Overview
--------

Wheel variants introduce a more fine-grained specification of built
wheel characteristics beyond what existing wheel tags provide.
Individual wheels carry a human-readable label defined at build time, as
described in `modified wheel filename`_, and are characterizing using
`variant property system`_. The properties are organized into a
hierarchical structure of namespaces, features and feature values. When
evaluating wheels to install, the installer determines whether variant
properties of a given wheel are compatible with the system, and perform
`variant ordering`_ based on the priority of the compatible variant
properties. This is done in addition to determining the compatibility.
The ordering by variant properties takes precedence over ordering by
tags.

Every variant namespace is governed by a variant provider. There are two
kinds of variant providers: install-time providers and ahead-of-time
(AoT) providers. Install-time providers require plugins that are queried
while installing wheels to determine the set of supported properties and
their preference order. For AoT providers, this data is static and
embedded in the wheel; it can be either provided directly by the
wheel maintainer or queried at wheel build time from an AoT plugin.

Both kinds of plugins are usually implemented as Python packages which
implement the `provider plugin API`_, but they may also be vendored or
reimplemented by installers to improve user experience, as outlined in
`Providers`_. Plugin packages may be installed in isolated or
non-isolated environments. In particular, all plugins may be returned by
the ``get_requires_for_build_wheel()`` hook of a :pep:`517` backend, and
therefore installed along with other build dependencies. For this
reason, it is important that plugin packages do not narrowly pin
dependencies, as that could prevent different packages from being
installed simultaneously in the same environment.

Metadata governing variant support is defined in ``pyproject.toml``
file, and it is copied into ``variant.json`` file in wheels, as explored
in `metadata in source tree and wheels`_. Additionally, `variant
environment markers`_ can be used to define dependencies specific to a
subset of variants.


Modified wheel filename
-----------------------

One of the core requirements of the design is to ensure that installers
predating this PEP will ignore wheel variant files. This makes it
possible to publish both variant wheels and non-variant wheels on a
single index, with installers that do not support variants securely
ignoring the former, and falling back to the latter.

A variant label component is added to the filename for the twofold
purpose of providing a unique mapping from the filename to a set of
variant properties, and providing a human-readable identification for
the variant. The label is kept short and lowercase to avoid issues with
different filesystems. It is added as a ``-``-separated component at the
end to ensure that the existing filename validation algorithms reject
it:

- If both the build tag and the variant label are present, the filename
  contains too many components. Example:

  .. code-block:: text

     numpy-2.3.2-1-cp313-cp313t-musllinux_1_2_x86_64-x86_64_v3.whl
                                                    ^^^^^^^^^^

- If only the variant label is present, the Python tag at third position
  will be misinterpreted as a build number. Since the build number must
  start with a digit and no Python tags at the time start with digits,
  the filename is considered invalid. Example:

  .. code-block:: text

     numpy-2.3.2-cp313-cp313t-musllinux_1_2_x86_64-x86_64_v3.whl
                 ^^^^^

This behavior was confirmed for a number of existing tools:
`auditwheel
<https://github.com/pypa/auditwheel/blob/6839107e9b918e035ab2df4927a25a5f81f1b8b6/src/auditwheel/repair.py#L61-L64>`__,
`packaging
<https://github.com/pypa/packaging/blob/78c2a5e4f5c04fd782a5729d93892c3a3eafe365/src/packaging/utils.py#L94-L134>`__,
`pdm
<https://github.com/pdm-project/pdm/blob/66c86908c9b9d07ad8d101d07879d69a55de5c54/src/pdm/models/requirements.py#L259-L286>`__,
`pip
<https://github.com/pypa/pip/blob/c46141c29c3646a3328bc4e51d354cc732fb1432/src/pip/_internal/models/wheel.py#L38-L46>`__,
`poetry
<https://github.com/python-poetry/poetry/blob/1c04c65149776ae4993fa508bef53373f45c66eb/src/poetry/utils/wheel.py#L23-L27>`__,
and `uv
<https://github.com/astral-sh/uv/blob/f6a9b55eb73be4f1fb9831362a192cdd8312ab96/crates/uv-distribution-filename/src/wheel.rs#L182-L299>`__.


Variant property system
-----------------------

Variant properties serve the purpose of expressing the characteristics
of the variant. Unlike platform compatibility tags, they are stored in
the variant metadata and therefore do not affect the wheel filename
length. They follow a hierarchical key-value design, with the key
further broken into a namespace and a feature name. Namespaces are used
to group features defined by a single provider, and to avoid conflicts
should multiple providers define a feature with the same name. This
permits independent governance and evolution of every namespace.

The keys are restricted to lowercase letters, digits, and underscores.
Uppercase characters are disallowed to avoid different spellings of the
same name. The character set for values is more relaxed, to permit
values resembling versions.

Variant properties are serialized into a structured 3-tuple format
inspired by Trove Classifiers in :pep:`301`:

.. code-block:: text

    {namespace} :: {feature_name} :: {feature_value}

Properties are used both to determine variant wheel compatibility, and
to select the best variant to install. Provider plugins indicate which
variant properties are compatible with the system, and order them by
importance. This ordering can further be altered in variant wheel
metadata.

Variant features can be declared as allowing multiple values to be
present within a single variant wheel. If that is the case, these values
are matched as a logical OR, i.e. only a single value needs to be
compatible with the system for the wheel to be considered supported. On
the other hand, features are treated as a logical AND, i.e. all of them
need to be compatible. This provides some flexibility in designating
variant compatibility while avoiding having to implement a complete
boolean logic.

Typically, variant features will be single-value and indicate minimal or
mutually exclusive requirements. The system may indicate multiple
compatible values. For example, if the feature declares a minimum CUDA
runtime version, the provider will indicate compatibility with wheels
requiring a minimum version corresponding to the currently installed
version or older, e.g. for CUDA 12.8, the compatible minimum versions
used in wheels would be, in order of decreasing preference:

.. code-block:: text

    nvidia :: cuda_version_lower_bound :: 12.8
    nvidia :: cuda_version_lower_bound :: 12.7
    nvidia :: cuda_version_lower_bound :: 12.6
    ...

Similarly, a wheel could indicate its minimum required CPU version, and
the provider will indicate all the compatible CPU versions.

Multi-value features are useful for "fat" packages where multiple
incompatible targets are supported by a single package. A typical
example are GPUs. In this case, the wheel declares a number of supported
GPUs, and the provider indicates which GPUs are actually installed
(usually one). The wheel is compatible if there is overlap between the
two lists.


Null variant
------------

A null variant is a variant wheel with no properties, but distinct
from non-variant wheels in having the ``null`` variant label and variant
metadata. During the transition period, it provides the possibility of
providing a distinct fallback for systems that do not support any of
the variants provided, and for systems that do support variant wheels at
all.

For example, a package with optional GPU support could publish three
kinds of wheels:

- Multiple GPU-enabled wheels, each built for a single CUDA version with
  a matching set of supported GPUs, and used only when the provider
  plugin indicates that the system is compatible.

- A CPU-only null variant, much smaller than the GPU variants, installed
  when the provider plugin indicates that no compatible GPU is
  installed.

- A GPU+CPU non-variant wheel, that will be installed on systems without
  an installer supporting variants.

Publishing a null variant is optional, and makes sense only if distinct
fallbacks provide advantages to the user. If one is published, a wheel
variant-enabled installer will prefer it over the non-variant wheel. If
it is not, it will fall back to the non-variant wheel instead. The
non-variant wheel is also used if variant support is explicitly disabled
by an installer flag.

The null variant uses a reserved ``null`` label to make it clearly
distinguishable from regular variants.


Install-time and Ahead-of-Time providers
----------------------------------------

The variant wheel metadata specifies what providers are used for its
properties. Providers serve a twofold purpose:

a. at install time: determining which variant wheels are compatible with
   the user's system, and which of them constitutes the best choice, and

b. at build time: determining which variant properties are valid for
   building a wheel.

The specification proposes two kinds of providers: install-time
providers and Ahead-of-Time providers.

Install-time providers are implemented either as Python packages that
need to be installed and run to query them, or vendored or reimplemented
in the tools. They are used when user systems need to be queried to
determine wheel compatibility, for example for variants utilizing GPUs
or requiring CPU instruction sets beyond what platform tags provide.
Installing third-party packages involves security risks highlighted in
the `security implications`_ section, and the proposed mitigations incur
a cost on installer implementations.

Ahead-of-Time providers are implemented as static metadata embedded in
the wheel. They are used when particular variant properties are always
compatible with the user's system (provided that a wheel using them has
been built successfully). However, the metadata indicates which
properties are preferred. For example, AoT providers can be used to
provide choice between builds against different BLAS / LAPACK providers,
or to provide debug builds of packages. Since they do not require
running code external to the installer, they do not pose the problems
faced by install-time providers, and can be used more liberally.

AoT providers are permitted to feature plugin packages. If that is the
case, these packages are only used when building wheels, and their
output is used to fill in the static metadata used at install time.
This way, it is easier to use consistent property names and values
across multiple packages. Otherwise, the package maintainer needs to
include the supported properties directly in the ``pyproject.toml``
file.

When implemented as Python packages, both kinds of provider plugins
expose roughly the same API. However, an AoT provider must always
consider all valid variant properties supported, and it must always
return the same ordered list of supported properties irrespective of the
user system. All AoT providers can technically be used as install-time
providers, but not the other way around.


Plugin stability and versioning
-------------------------------

As the specification introduces the potential necessity of installing
and running provider packages to install wheels, it is recommended that
these packages remain functioning correctly for the variant wheels
published in the past, including very old package versions. Ideally, no
properties previously supported should ever be removed.

If a breaking change needs to be performed, it is recommended to either
introduce a new provider package for that, or add a new plugin API
endpoint to the existing package. In both cases, it may be necessary to
preserve the old endpoint in minimal maintenance mode, to ensure that
old wheels can still be installed. The old endpoint can trigger
deprecation warnings in the ``get_all_configs()`` hook that is used when
building packages.

An alternative approach is to use semantic versioning to cut off
breaking changes. However, this relies on package authors reliably using
caps on dependencies, as otherwise old wheels will start using
incompatible plugin versions. This is already a problem with Python
build backends used today.

When vendoring or reimplementing plugins, installers need to follow
their current behavior. In particular, they should recognize the
relevant provider versions numbers, and possibly fall back to installing
the external plugin when the package in question is incompatible with
the installer's implementation.


Metadata in source tree and wheels
-----------------------------------

Variants introduce a few new portions of metadata that are stored in the
source tree and in wheels. In the source tree, it is stored in the
``pyproject.toml`` file along with other project properties, benefiting
from the TOML format's readability and strictness. Afterwards, it is
converted into an equivalent JSON structure, and stored as a separate
file in the ``.dist-info`` directory. The existing metadata files are
unchanged to avoid unnecessary incompatibility, and to avoid serializing
into the inconvenient :doc:`Core Metadata
<packaging:specifications/core-metadata>` format.

The metadata in ``pyproject.toml`` includes:

- information about variant providers that could be used by the wheels,
- optionally, lists overriding the default property ordering,
- static property lists for Ahead-of-Time providers that do not use
  plugins.

In wheel metadata, the above is amended by static property lists
obtained from the plugins and variant properties for the built wheel.

When wheels are published on an index, the variant metadata from all
wheels is combined into a single ``{name}-{version}-variants.json`` file
that is used by clients to efficiently obtain the variant metadata
without having to download it from individual wheels separately, or
implement explicit variant metadata support in an API provided by the
package index server.


ABI dependency variant provider
-------------------------------

Some packages provide extension modules exposing an Application Binary
Interface (ABI) that is not compatible across wide ranges of versions.
The packages using this interface need to pin their wheels to the
version used at build time. If ABI changes frequently, the pins are very
narrow and users face problems if they need to install two packages that
may happen to pin to different versions of the same dependency.
Providing variants built against different dependency versions can
increase the chance of a resolver being able to find a dependency version
that is compatible with all the packages being installed.

Unfortunately, such a variant provider cannot be implemented within the
plugin API defined by the specification. Given that a robust
implementation would need to interface with the dependency resolver,
rather than attempt to extend the API to cover this use case and add
significant complexity as a result, the specification reserves
``abi_dependency`` as a special variant namespace that can be
implemented by installers wishing the provide this feature.

Given the complexity of the problem, this extension is made entirely
optional. This implies that any packages using it need to provide
non-variant wheels as well.


Suggested implementation logic for a packaging tool
---------------------------------------------------------

Installing a package from an index
''''''''''''''''''''''''''''''''''

.. figure:: pep-0817/conceptual_diagram_installers.png
    :target: _images/conceptual_diagram_installers.png
    :class: invert-in-dark-mode
    :alt: A diagram showing installing a package including variant wheel
          building. It is split into three columns: Developer, Installer
          and Install-time providers. The diagram starts with Develop
          initiating package install. The subsequent steps involve
          installer, in order: resolver selects package version;
          determine if release has variant wheels. If there are no
          variant wheels, jump to installing package and report success.
          If the version has variant wheels, check user's variant
          preferences. In parallel, download JSON from index, then
          extract variant provider configuration. If it uses AoT
          providers only, converge to determine optimal variant
          immediately. If it requires install-time providers, the
          further path depends on whether non-vendored providers are
          included. If they are not, query install-time providers
          immediately and converge to determine optimal variant. If
          non-vendored providers are included, they are installed if not
          present in env and then queried. Querying providers involves
          an exchange of data with different providers (in the diagram,
          "provider 1" and "provider 2" are given as examples), each
          filtering and ordering supported configurations for the
          current environment. All the variant paths converge on
          determining optimal variant, which is following by installing
          package and reporting success.

    A conceptual diagram of installing a wheel.

When asked to install a version of a package from an index, the proposed
tool behavior would be to:

1. Query the remote index for the desired package.
2. Select an initial match for a package version meeting the version constraints,
   as usual (this does not need to take variant metadata into account).
3. Filter available wheels based on Platform Compatibility Tags.
4. Determine if any of the remaining wheels are variant wheels.
   If not, proceed as with non-variant wheels.
5. If any wheels feature variant labels, download the index-level
   variant metadata file, ``{name}-{version}-variants.json``. If this
   file is missing, assume all variant wheels are incompatible and
   proceed as with non-variant wheels.
6. Map the variant labels into sets of variant properties using the
   index-level variant metadata file. If any of the labels present in
   wheel filenames are missing in the file, assume that the respective
   wheels are incompatible.
7. Obtain the ordered lists of supported variant properties using
   providers specified in the index-level variant metadata file:

   - for the enabled AoT providers, obtain them from static property
     data in the index-level variant metadata file.
   - for the enabled install-time providers:

     - if the user provided static compatibility information, use that.
     - otherwise, if the provider is vendored or reimplemented, query it
       in implementation-specific manner.
     - otherwise, if the Python provider package is considered secure
       (either by the installer or via explicit user opt-in), install it
       in an isolated environment, and query it via the plugin API.
     - if none of the above applies, do not run the provider and either
       consider the variant properties incompatible, or fail the
       installation.

   - for the disabled providers (e.g. opt-in providers that were not
     enabled by the user, providers excluded via environment markers),
     assume that all variant properties in the namespace are
     incompatible.

8. Filter and order variants based on the lists of supported properties,
   and select the most preferred variant. If no variant wheel matched,
   use the non-variant wheels by their rules.
9. If multiple wheels for a given version share the same variant label,
   order them by Platform compatibility tags and build number, and
   select the best wheel.


Installing a local wheel
''''''''''''''''''''''''

When asked to install a local wheel file, the tool's proposed behavior would be
to:

1. If no variant label is present in the filename, proceed as with
   non-variant wheels.
2. Verify the wheel compatibility via Platform compatibility tags.
3. Read variant metadata from ``*.dist-info/variant.json`` inside the
   wheel file.
4. Obtain the ordered lists of supported variant properties, as when
   `installing a package from an index`_.
5. Verify the wheel compatibility via supported properties.


Building a variant wheel
''''''''''''''''''''''''

In order to build a variant wheel, the build backend needs to receive a
list of variant properties and a variant label. The recommended way to
do that is to use backend-defined keys in the ``config_settings``
dictionary passed to the build backend hooks.

When building a variant wheel, the proposed behavior for the build
backend would be to:

1. Read variant provider metadata from ``pyproject.toml``.
2. Verify that all namespaces specified in the user-defined variant
   properties have a corresponding provider in the metadata.
3. In the ``get_requires_for_build_wheel()`` hook, return variant
   provider plugin packages along with other build dependencies.
4. In the ``build_wheel()`` hook, query the provider plugins
   ``get_all_configs()`` function to obtain all valid property keys and
   values. Use it to verify that the specified properties are correct.
5. Convert the variant metadata from ``pyproject.toml`` to JSON, append
   the mapping from variant label to variant properties and write the
   result into the wheel's ``*.dist-info/variant.json`` file.
6. Build the wheel as usual, except for including the
   ``*.dist-info/variant.json`` and the variant label in the filename.


Publishing variant wheels on an index
'''''''''''''''''''''''''''''''''''''

Variant wheels are uploaded to an index just like regular wheels.
There are two possible approaches to publishing the index-level
``{name}-{version}-variants.json`` file for every package version:
it can either be prepared and uploaded by the user, or it can be
generated automatically by the index.

The file should not be changed once it is published, as clients may have
already cached it or locked to the existing hash. For this reason, if
the index is responsible for generating the file, it should use some
mechanism to defer publishing it until the release is fully uploaded
(for example, :pep:`694`).

To generate the ``{name}-{version}-variants.json`` file:

1. For the first variant wheel for a given package version, copy the
   data from its ``*.dist-info/variant.json`` file.
2. For subsequent wheels, merge the data from their
   ``*.dist-info/variant.json`` files into the existing data:

   - disjoint keys of ``providers``, ``static-properties`` and
     ``variants`` dictionaries are merge together
   - common keys of these dictionaries must have exactly the same value
   - ``default-priorities.namespace`` list can be replaced if the new
     value starts with the old value
   - ``default-priorities.feature`` and ``default-priorities.value``
     keys can be added if they were not present in the previous
     ``default-priorities.namespace`` value
   - other keys must have exactly the same value


Example use cases
-----------------

PyTorch CPU/GPU variants
''''''''''''''''''''''''

As of October 2025, `PyTorch
<https://pytorch.org/get-started/locally/>`__ publishes a total of seven
variants for every release: a CPU-only variant, three CUDA variants with
different minimal CUDA runtime versions and supported GPUs, two ROCm
variants and a Linux XPU variant.

This setup could be improved using GPU/XPU plugins that query the
installed runtime version and installed GPUs/XPUs to filter out the
wheels for which the runtime is unavailable, it is too old or the user's
GPU is not supported, and order the remaining variants by the runtime
version. The CPU-only version is published as a null variant that is
always supported.

If a GPU runtime is available and supported, the installer automatically
chooses the wheel for the newest runtime supported. Otherwise, it falls
back to the CPU-only variant. In the corner case when multiple
accelerators are available and supported, PyTorch package maintainers
indicate which one takes preference by default.


Optimized CPU variants
''''''''''''''''''''''

Wheel variants can be used to provide variants requiring specific CPU
extensions, beyond what platform tags currently provide. They can be
particularly helpful when runtime dispatching is impractical, when the
package relies on prebuilt components that use instructions above the
baseline, when availability of instruction sets implies library ABI
changes, or simply to benefit from compiler optimizations such as
auto-vectorization applied across the code base.

For example, an x86-64 CPU plugin can detect the capabilities for the
installed CPU, mapping them onto the appropriate x86-64 architecture
level and a set of extended instruction sets. Variant wheels indicate
which level and/or instruction sets are required. The installer filters
out variants that do not meet the requirements and select the best
optimized variant. A non-variant wheel can be used to represent the
architecture baseline, if supported.

Implementation using wheel variants makes it possible to provide
fine-grained indication of instruction sets required, with plugins that
can be updated as frequently as necessary. In particular, it is neither
necessary to cover all available instruction sets from the start, nor to
update the installers whenever the instruction set coverage needs to be
improved.


BLAS / LAPACK variants
''''''''''''''''''''''

Packages such as NumPy_ and SciPy_ can be built using different BLAS /
LAPACK libraries. Users may wish to choose a specific library for
improved performance on a particular hardware, or based on license
considerations. Furthermore, different libraries may use different
OpenMP implementations, whereas using a consistent implementation across
the stack can avoid degrading performance through spawning too many
threads.

BLAS / LAPACK variants do not require a plugin at install time, since
all variants built for a particular platform are compatible with it.
Therefore, an ahead-of-time provider (with ``install-time = false``)
that provides a predefined set of BLAS / LAPACK library names can be
used. When the package is installed, normally the default variant is
used, but the user can explicitly select another one.


Debug package variants
''''''''''''''''''''''

A package may wish to provide a special debug-enabled builds for
debugging or CI purposes, in addition to the regular release build. For
this purpose, an optional ahead-of-time provider can be used
(``install-time = false`` with ``optional = true``), defining a custom
property for the debug builds. Since the provider is disabled by
default, users normally install the non-variant wheel providing the
release build. However, they can easily obtain the debug build by
enabling the optional provider or selecting the variant explicitly.


Package ABI matching
''''''''''''''''''''

Packages such as vLLM_
need to be pinned to the PyTorch version they were built against to
preserve Application Binary Interface (ABI) compatibility. This often
results in unnecessarily strict pins in package versions, making it
impossible to find a satisfactory resolution for an environment
involving multiple packages requiring different versions of PyTorch, or
resorting to source builds. Variant wheels can be used to publish
variants of vLLM built against different PyTorch versions, therefore
enabling upstream to easily provide support for multiple versions
simultaneously.

The optional ``abi_dependency`` extension can be used to build multiple
``vllm`` variants that are pinned to different PyTorch versions, e.g.:

- ``vllm-0.11.0-...-torch29.wheel`` with
  ``abi_dependency :: torch :: 2.9``
- ``vllm-0.11.0-...-torch28.wheel`` with
  ``abi_dependency :: torch :: 2.8``
- ``vllm-0.11.0-...-torch27.wheel`` with
  ``abi_dependency :: torch :: 2.7``


Security implications
=====================

The proposal introduces a plugin system for querying the system
capabilities in order to determine variant wheel capability. The system
permits specifying additional Python packages providing the plugins
in the package index metadata. Installers and other tools that need
to determine whether a particular wheel is installable, or select
the most preferred variant among multiple variant wheels, may need
to install these packages and execute the code within them while
resolving dependencies or processing wheels.

This elevates the supply-chain attack potential by introducing two new
points for malicious actors to inject arbitrary code payload:

1. Publishing a version of a variant provider plugin or one of its
   dependencies with malicious code.

2. Introducing a malicious variant provider plugin in an existing
   package metadata.

While such attacks are already possible at the package dependency level,
it needs to be emphasized that in some scenarios the affected tools are
executed with elevated privileges, e.g. when installing packages for
multi-user systems, while the installed packages are only used with
regular user privileges afterwards. Therefore, variant provider plugins
could introduce a Remote Code Execution vulnerability with elevated
privileges.

A similar issue already exists in the packaging ecosystem when packages
are installed from source distributions, whereas build backends
and other build dependencies are installed and executed. However,
various tools operating purely on wheels, as well as users using
tool-specific options to disable use of source distributions,
have been relying on the assumption that no code external to the system
will be executed while resolving dependencies, installing a wheel or
otherwise processing it. To uphold this assumption, the proposal
explicitly requires that untrusted provider plugin packages are never
installed without explicit user consent.

The `Providers`_ section of the specification provides further
suggestions that aim to improve both security and the user experience.
Particularly, it is expected that the most popular provider plugins will
be available out of the box, and a dedicated team of maintainers
(initially including a subset of the PEP authors) will be responsible
for inspecting them for security risks and vetting the plugins as safe
to use. Installers will be able to either use a published allowlist,
vendor specific provider plugin versions, reimplement them or use them
as a Python library at their leisure.

This will lead to the majority of packages focusing on these specific
plugins, rather than implementing competing solutions. Plugins requiring
explicit opt-in should be rare, and primarily affect expert users. This
is important to make variant usage secure-by-default. Furthermore, the
frequent disruption of workflows incentivises users to blanket-allow all
plugins (security fatigue).

Furthermore, the specification permits using static configuration as
input to skip running plugins altogether.


Specification
=============

This PEP proposes a set of extensions to the
:ref:`packaging:binary-distribution-format` specification that enable
building additional variants of wheels that can be installed by
variant-aware tools while being ignored by programs that do not
implement this specification.


Definitions
-----------

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT",
"SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this
document are to be interpreted as described in :rfc:`2119`.


Extended wheel filename
-----------------------

The wheel filename template originally defined by :pep:`427` is changed
to:

.. code:: text

    {distribution}-{version}(-{build tag})?-{python tag}-{abi tag}-{platform tag}(-{variant label})?.whl
                                                                                 +++++++++++++++++++

Wheels using extensions introduced by this PEP MUST feature the variant
label component. The label MUST adhere to the following rules:

- Lower case only (to prevent issues with case-sensitive
  vs. case-insensitive filesystems)
- Between 1-16 characters
- Using only ``0-9``, ``a-z``, ``.`` or ``_`` ASCII characters

This is equivalent to the following regular expression:
``^[0-9a-z._]{1,16}$``.

Every label MUST uniquely correspond to a specific set of variant
properties, which MUST be the same for all wheels using the same label
within a single package version. Variant labels SHOULD be specified at
wheel build time, as human-readable strings. The label ``null`` is
reserved for the null variant and MUST use an empty set of variant
properties.

Installers that do not implement this specification MUST ignore wheels
with variant label when installing from an index, and fall back to a
wheel without such label if it is available. If no such wheel is
available, the installer SHOULD output an appropriate diagnostic,
in particular warning if it results in selecting an earlier package
version or a clear error if no package version can be installed.

Examples:

- Non-variant wheel:
  ``numpy-2.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl``
- Wheel with variant label:
  ``numpy-2.3.2-cp313-cp313t-musllinux_1_2_x86_64-x86_64_v3.whl``


Variant properties
------------------

Every variant wheel MUST be described by zero or more variant
properties. A variant wheel with exactly zero properties represents the
null variant. The properties are specified when the variant wheel is
being built, using a mechanism defined by the project's build backend.

Each variant property is described by a 3-tuple that is serialized into
the following format:

::

    {namespace} :: {feature_name} :: {feature_value}

The namespace MUST consist only of ``0-9``, ``a-z`` and ``_`` ASCII
characters (``^[a-z0-9_]+$``). It MUST correspond to a single variant
provider.

The feature name MUST consist only of ``0-9``, ``a-z`` and ``_`` ASCII
characters (``^[a-z0-9_]+$``). It MUST correspond to a valid feature
name defined by the respective variant provider in the namespace.

The feature value MUST consist only of ``0-9``, ``a-z``, ``_`` and ``.``
ASCII Characters (``^[a-z0-9_.]+$``). It MUST correspond to a valid
value defined by the respective variant provider for the feature.

If a feature is marked as "multi-value" by the provider plugin, a single
variant wheel can define multiple properties sharing the same namespace
and feature name. Otherwise, there MUST NOT be more than a single value
corresponding to a single pair of namespace and feature name within a
variant wheel.

For a variant wheel to be considered compatible with the system, all of
the features defined within it MUST be determined to be compatible. For
a feature to be compatible, at least a single value corresponding to it
MUST be compatible.

Examples:

.. code:: text

    # all of the following must be supported
    x86_64 :: level :: v3
    x86_64 :: avx512_bf16 :: on
    nvidia :: cuda_version_lower_bound :: 12.8
    # additionally, at least one of the following must be supported
    nvidia :: sm_arch :: 120_real
    nvidia :: sm_arch :: 110_real


Providers
---------

When installing or resolving variant wheels, installers SHOULD query the
variant providers to verify whether a given wheel's properties are
compatible with the system and to select the best variant through
`variant ordering`_. However, they MAY provide an option to omit the
verification and install a specified variant explicitly.

Providers can be marked as install-time or ahead-of-time. For
install-time providers, installers MUST either query the provider for
variant property compatibility, or use user-provided compatibility
information. Installers MAY vendor or reimplement specific providers.
The format of user-provided information is left implementation-defined.

For ahead-of-time providers, they MUST use the static metadata embedded
in the wheel instead.

Providers can be marked as optional. If a provider is marked optional,
then the installer MUST NOT query said provider by default, and instead
assume that its properties are incompatible. It SHOULD provide an option
to enable optional providers.

Providers can also be made conditional to
:ref:`dependency-specifiers-environment-markers`. If that is the case,
the installer MUST check the markers against the environment to which
wheels are going to be installed. It MUST NOT use any providers whose
markers do not match, and instead assume that their properties are
incompatible.

All the tools that need to query variant providers and are run in a
security-sensitive context, MUST NOT install or run provider packages,
unless they can determine the particular provider package version to be
trusted. The exact mechanism used to do that is implementation-specific.
However, installers SHOULD ensure that the most commonly used providers
can be securely used without an explicit user opt-in.

When installing provider packages, tools SHOULD use an isolated virtual
environment.

Install-time provider packages SHOULD take measures to guard against
supply chain attacks, for example by vendoring all dependencies.

For a consistent experience between tools, variant wheels SHOULD be
supported by default. Tools MAY provide an option to only use
non-variant wheels.


Variant metadata
----------------

This section describes the metadata format for the providers, variants
and properties of a package and its wheels. The format is used in three
locations, with slight variations:

1. in the source tree, inside the ``pyproject.toml`` file
2. in the built wheel, as a ``*.dist-info/variant.json`` file
3. on the package index, as a ``{name}-{version}-variants.json`` file.

All three variants metadata files share a common JSON-compatible
structure:

.. code:: text

    (root)
    |
    +- providers
    |  +- {namespace}
    |     +- enable-if     : str | None = None
    |     +- install-time  : bool       = True
    |     +- optional      : bool       = False
    |     +- plugin-api    : str | None = None
    |     +- requires      : list[str]  = []
    |
    +- default-priorities
    |  +- namespace        : list[str]
    |  +- feature
    |     +- {namespace}   : list[str]  = []
    |  +- property
    |     +- {namespace}
    |        +- {feature}  : list[str]  = []
    |
    +- static-properties
    |  +- {namespace}
    |     +- {feature}     : list[str]  = []
    |
    +- variants
      +- {variant_label}
         +- {namespace}
            +- {feature}   : list[str]  = []

The top-level object is a dictionary rooted at a specific point in the
containing file. Its individual keys are sub-dictionaries that are
described in the subsequent sections, along with the requirements for
their presence. The tools MUST ignore unknown keys in the dictionaries
for forwards compatibility of updates to the PEP. However, users
MUST NOT use unsupported keys to avoid potential future conflicts.

A `JSON schema <https://json-schema.org/>`__ is included in the Appendix
of this PEP, to ease comprehension and validation of the metadata
format. This schema will be updated with each revision to the variant
metadata specification. The schema is available in
:ref:`0817-variant-json-schema`.

Ultimately, the variant metadata JSON schema SHOULD be served by
`packaging.python.org <https://packaging.python.org/>`__.

Provider information
''''''''''''''''''''

``providers`` is a dictionary, the keys are namespaces, the values are
dictionaries with provider information. It specifies how to install and
use variant providers. A provider information dictionary MUST be
declared in ``pyproject.toml`` for every variant namespace supported by
the package. It MUST be copied to ``variant.json`` as-is, including
the data for providers that are not used in the particular wheel.

The use of provider information is described in the `Providers`_ and
`Provider plugin API`_ sections.

A provider information dictionary MAY contain the following keys:

- ``enable-if: str``: An :ref:`environment marker
  <dependency-specifiers-environment-markers>` defining when the plugin
  should be used.

- ``install-time: bool``: Whether this is an install-time provider.
  Defaults to ``true``. ``false`` means that it is an AoT provider
  instead.

- ``optional: bool``: Whether the provider is optional. Defaults
  to ``false``. If it is ``true``, the provider is
  considered optional.

- ``plugin-api: str``: The API endpoint for the plugin. If it is
  specified, it MUST be an object reference as explained in the `API
  endpoint`_ section. If it is missing, the package name from the first
  dependency specifier in ``requires`` is used, after replacing all
  ``-`` characters with ``_`` in the normalized package name.

- ``requires: list[str]``: A list of zero or more package
  :ref:`dependency specifiers <dependency-specifiers>`, that are used to
  install the provider plugin. If the dependency specifiers include
  environment markers, these are evaluated against the environment where
  the plugin is being installed and the requirements for which the
  markers evaluate to false are filtered out. In that case, at least
  one dependency MUST remain present in every possible environment.
  Additionally, if ``plugin-api`` is not specified, the first dependency
  present after filtering MUST always evaluate to the same API endpoint.

All the fields are OPTIONAL, with the following exceptions:

1. If ``install-time`` is true, the dictionary describes an install-time
   provider and the ``requires`` key MUST be present and specify at
   least one dependency.

2. If ``install-time`` is false, it describes an AoT provider and the
   ``requires`` key is OPTIONAL. In that case:

   a. If ``requires`` is provided and non-empty, the provider dictionary
      MUST reference an AoT provider plugin that will be queried at
      build time to fill ``static-properties``.

   b. Otherwise, ``static-properties`` MUST be specified in
      ``pyproject.toml``.


Default priorities
''''''''''''''''''

The ``default-priorities`` dictionary controls the ordering of variants.
The exact algorithm is described in the `Variant ordering`_ section.

It has a single REQUIRED key:

- ``namespace: list[str]``: All namespaces used by the wheel variants,
  ordered in decreasing priority. This list MUST have the same members
  as the keys of the ``providers`` dictionary.

It MAY have the following OPTIONAL keys:

- ``feature: dict[str, list[str]]``: A dictionary with namespaces as
  keys, and ordered list of corresponding feature names as values. The
  values in each list override the default ordering from the provider
  output. They are listed from the highest priority to the lowest
  priority. Features not present on the list are considered of lower
  priority than those present, and their relative priority is defined by
  the plugin.

- ``property: dict[str, dict[str, list[str]]]``: A nested dictionary
  with namespaces as first-level keys, feature names as second-level
  keys and ordered lists of corresponding property values as
  second-level values. The values present in the list override the
  default ordering from the provider output. They are listed from the
  the highest priority to the lowest priority. Properties not present on
  the list are considered of lower priority than these present, and
  their relative priority is defined by the plugin output.


Static properties
'''''''''''''''''

The ``static-properties`` dictionary specifies the supported properties
for AoT providers. It is a nested dictionary with namespaces as first
level keys, feature name as second level keys and ordered lists of
feature values as second level values.

In ``pyproject.toml`` file, the namespaces present in this dictionary
MUST correspond to all AoT providers without a
plugin (i.e. with ``install-time`` of ``false`` and no or empty
``requires``). When building a wheel, the build backend MUST query the
AoT provider plugins (i.e. these with ``install-time`` being ``false``
and non-empty ``requires``) to obtain supported properties and embed
them into the dictionary. Therefore, the dictionary in ``variant.json``
and ``*-variants.json`` MUST contain namespaces for all AoT providers
(i.e. all providers with ``install-time`` being ``false``).

Since TOML and JSON dictionaries are unsorted, so are the features in
the ``static-properties`` dictionary. If more than one feature is
specified for a namespace, then the order for all features MUST be
specified in ``default-priorities.feature.{namespace}``. If an AoT
plugin is used to fill ``static-properties``, then the features not
already in the list in ``pyproject.toml`` MUST be appended to it.

The list of values is ordered from the most preferred to the least
preferred, same as the lists returned by ``get_supported_configs()``
plugin API call (as defined in `plugin interface`_). The
``default-priorities.property`` dict can be used to override the
property ordering.


Variants
''''''''

The ``variants`` dictionary is used in ``variant.json`` to indicate the
variant that the wheel was built for, and in ``*-variants.json`` to
indicate all the wheel variants available. It's a 3-level dictionary
listing all properties per variant label: The first level keys are
variant labels, the second level keys are namespaces, the third level
are feature names, and the third level values are lists of feature
values.


``pyproject.toml``: variant project-level data table
''''''''''''''''''''''''''''''''''''''''''''''''''''

The ``pyproject.toml`` file is the standard project configuration file
as defined in :doc:`packaging:specifications/pyproject-toml`. The
variant metadata MUST be rooted at a top-level table named ``variant``.
It MUST NOT specify the ``variants`` dictionary. It is used by build
backends to build variant wheels.

Example Structure:

.. code:: toml

    [variant.default-priorities]
    # prefer CPU features over BLAS/LAPACK variants
    namespace = ["x86_64", "aarch64", "blas_lapack"]

    # prefer aarch64 version and x86_64 level features over other features
    # (specific CPU extensions like "sse4.1")
    feature.aarch64 = ["version"]
    feature.x86_64 = ["level"]

    # prefer x86-64-v3 and then older (even if CPU is newer)
    property.x86_64.level = ["v3", "v2", "v1"]

    [variant.providers.aarch64]
    # example using different package based on Python version
    requires = [
       "provider-variant-aarch64 >=0.0.1; python_version >= '3.12'",
       "legacy-provider-variant-aarch64 >=0.0.1; python_version < '3.12'",
    ]
    # use only on aarch64/arm machines
    enable-if = "platform_machine == 'aarch64' or 'arm' in platform_machine"
    plugin-api = "provider_variant_aarch64.plugin:AArch64Plugin"

    [variant.providers.x86_64]
    requires = ["provider-variant-x86-64 >=0.0.1"]
    # use only on x86_64 machines
    enable-if = "platform_machine == 'x86_64' or platform_machine == 'AMD64'"
    plugin-api = "provider_variant_x86_64.plugin:X8664Plugin"

    [variant.providers.blas_lapack]
    # plugin-api inferred from requires
    requires = ["blas-lapack-variant-provider"]
    # plugin used only when building package, properties will be inlined
    # into variant.json
    install-time = false


``*.dist-info/variant.json``: the packaged variant metadata file
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

The ``variant.json`` file MUST be present in the ``*.dist-info/``
directory of a built variant wheel. It is serialized into JSON, with the
variant metadata dictionary being the top object. It MUST include all
the variant metadata present in ``pyproject.toml``, copied as indicated
in the individual key sections. In addition to that, it MUST contain:

- a ``$schema`` key whose value is the URL corresponding to the schema
  file supplied in the appendix of this PEP. The URL contains the
  version of the format, and a new version MUST be added to the appendix
  whenever the format changes in the future,

- a ``variants`` object listing exactly one variant - the variant
  provided by the wheel.

The variant.json file corresponding to the wheel built from the example
pyproject.toml file for x86-64-v3 would look like:

.. code:: json5

    {
      // The schema URL will be replaced with the final URL on packaging.python.org
      "$schema": "https://variants-schema.wheelnext.dev/v0.0.3.json",
      "default-priorities": {
         "feature": {
            "aarch64": ["version"],
            "x86_64": ["level"]
         },
         "namespace": ["x86_64", "aarch64", "blas_lapack"],
         "property": {
            "x86_64": {
               "level": ["v3", "v2", "v1"]
            }
         }
      },
      "providers": {
         "aarch64": {
            "enable-if": "platform_machine == 'aarch64' or 'arm' in platform_machine",
            "plugin-api": "provider_variant_aarch64.plugin:AArch64Plugin",
            "requires": [
               "provider-variant-aarch64 >=0.0.1; python_version >= '3.12'",
               "legacy-provider-variant-aarch64 >=0.0.1; python_version < '3.12'"
            ]
         },
         "blas_lapack": {
            "install-time": false,
            "requires": ["blas-lapack-variant-provider"]
         },
         "x86_64": {
            "enable-if": "platform_machine == 'x86_64' or platform_machine == 'AMD64'",
            "plugin-api": "provider_variant_x86_64.plugin:X8664Plugin",
            "requires": ["provider-variant-x86-64 >=0.0.1"]
         }
      },
      "static-properties": {
         "blas_lapack": {
            "provider": ["accelerate", "openblas", "mkl"]
         },
      },
      "variants": {
         // always a single entry, expressing the variant properties of the wheel
         "x8664v3_openblas": {
            "blas_lapack": {
               "provider": ["openblas"]
            },
            "x86_64": {
               "level": ["v3"]
            }
         }
      }
    }


``{name}-{version}-variants.json``: the index level variant metadata file
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

For every package version that includes at least one variant wheel,
there MUST exist a corresponding ``{name}-{version}-variants.json``
file, hosted and served by the package index. The ``{name}`` and
``{version}`` placeholders correspond to the package name and version,
normalized according to the same rules as wheel files, as found in the
:ref:`packaging:wheel-file-name-spec` of the Binary Distribution Format
specification. The link to this file MUST be present on all index pages
where the variant wheels are linked. It is presented in the same simple
repository format as source distribution and wheel links in the index,
including an (OPTIONAL) hash.

This file uses the same structure as ``variant.json`` described above,
except that the variants object MUST list all variants available on the
package index for the package version in question. It is RECOMMENDED
that tools enforce the same contents of the ``default-priorities``,
``providers`` and ``static-properties`` sections for all variants listed
in the file, though careful merging is possible, as long as no
conflicting information is introduced, and the resolution results within
a subset of variants do not change.

The index MAY generate the index level variant metadata file
automatically from uploaded wheel metadata. If that is the case, the
file SHOULD NOT be published until it is final, and once published, it
SHOULD NOT change, as clients MAY cache it.

If the file is not generated automatically, the index MUST permit
package maintainers to upload it. Once the variant level metadata file
is uploaded, the package maintainers SHOULD NOT upload new variants for
the version in question.

The ``foo-1.2.3-variants.json`` corresponding to the package with two
wheel variants, one of them listed in the previous example, would look
like:

.. code:: json5

    {
      // The schema URL will be replaced with the final URL on packaging.python.org
      "$schema": "https://variants-schema.wheelnext.dev/v0.0.3.json",
      "default-priorities": {
         // identical to above
      },
      "providers": {
         // identical to above
      },
      "static-properties": {
         // identical to above
      },
      "variants": {
         // all available wheel variants
         "x8664v3_openblas": {
            "blas_lapack": {
               "provider": ["openblas"]
            },
            "x86_64": {
               "level": ["v3"]
            }
         },
         "x8664v4_mkl": {
            "blas_lapack": {
               "provider": ["mkl"]
            },
            "x86_64": {
               "level": ["v4"]
            }
         }
     }
    }


Variant ordering
----------------

To determine which variant wheel to install when multiple wheels are
compatible, variant wheels MUST be ordered by their variant properties.

For the purpose of ordering, variant properties are grouped into
features, and features into namespaces. The ordering MUST be equivalent
to the following algorithm:

1. Construct the ordered list of namespaces by copying the value of the
   ``default-priorities.namespace`` key.

2. For every namespace:

   i. Construct the initial ordered list of feature names by copying the
      value of the respective ``default-priorities.feature.{namespace}``
      key.

   ii. Obtain the supported feature names from the provider, in order.
       For every feature name that is not present in the constructed
       list, append it to the end.

   After this step, a list of ordered feature names is available for
   every namespace.

3. For every feature:

   i. Construct the initial ordered list of values by copying the value
      of the respective
      ``default-priorities.property.{namespace}.{feature_name}`` key.

   ii. Obtain the supported values from the provider, in order. For
       every value that is not present in the constructed list, append
       it to the end.

   After this step, a list of ordered property values is available for
   every feature.

4. For every variant property present in at least one of the compatible
   variant wheels, construct a sort key that is a 3-tuple consisting of
   its namespace, feature name and feature value indices in the
   respective ordered lists.

5. For every compatible variant wheel, order its properties by their
   sort keys, in ascending order.

6. To order variant wheels, compare their sorted properties. If the
   properties at the first position are different, the variant with the
   lower 3-tuple of the respective property is sorted earlier. If they
   are the same, compare the properties at the second position, and so
   on, until either a tie-breaker is found or the list of properties of
   one wheel is exhausted. In the latter case, the variant with more
   properties is sorted earlier.

After this process, the variant wheels are sorted from the most
preferred to the least preferred. The null variant naturally sorts after
all the other variants, and the non-variant wheel MUST be sorted after
the null variant. Multiple wheels with the same variant set (and
multiple non-variant wheels) MUST then be ordered according to their
platform compatibility tags.

Alternatively, the sort algorithm for variant wheels could be described
using the following pseudocode. For simplicity, this code does not
account for non-variant wheels or tags.

.. code:: python

    from typing import Self


    def get_supported_feature_names(namespace: str) -> list[str]:
        """Get feature names from plugin's get_supported_configs()"""
        ...


    def get_supported_feature_values(namespace: str, feature_name: str) -> list[str]:
        """Get feature values from plugin's get_supported_configs()"""
        ...


    # default-priorities dict from variant metadata
    default_priorities = {
        "namespace": [...],  # : list[str]
        "feature": {...},    # : dict[str, list[str]]
        "property": {...},   # : dict[str, dict[str, list[str]]]
    }


    # 1. Construct the ordered list of namespaces.
    namespace_order = default_priorities["namespace"]
    feature_order = {}
    value_order = {}

    for namespace in namespace_order:
        # 2. Construct the ordered lists of feature names.
        feature_order[namespace] = default_priorities["feature"].get(namespace, [])
        for feature_name in get_supported_feature_names(namespace):
            if feature_name not in feature_order[namespace]:
                feature_order[namespace].append(feature_name)

       value_order[namespace] = {}
       for feature_name in feature_order[namespace]:
            # 3. Construct the ordered lists of feature values.
            value_order[namespace][feature_name] = (
                default_priorities["property"].get(namespace, {}).get(feature_name, [])
            )
            for feature_value in get_supported_feature_values(namespace, feature_name):
                if feature_value not in value_order[namespace][feature_name]:
                    value_order[namespace][feature_name].append(feature_value)


    def property_key(prop: tuple[str, str, str]) -> tuple[int, int, int]:
        """Construct a sort key for variant property (akin to step 4.)"""
        namespace, feature_name, feature_value = prop
        return (
            namespace_order.index(namespace),
            feature_order[namespace].index(feature_name),
            value_order[namespace][feature_name].index(feature_value),
        )


    class VariantWheel:
        """Example class exposing properties of a variant wheel"""
        properties: list[tuple[str, str, str]]

        def __lt__(self: Self, other: Self) -> bool:
            """Variant comparison function for sorting (akin to step 6.)"""
            for self_prop, other_prop in zip(self.properties, other.properties):
                if self_prop != other_prop:
                    return property_key(self_prop) < property_key(other_prop)
            return len(self.properties) > len(other.properties)


    # A list of variant wheels to sort.
    wheels: list[VariantWheel] = [...]


    for wheel in wheels:
        # 5. Order variant wheel properties by their sort keys.
        wheel.properties.sort(key=property_key)
    # 6. Order variant wheels by comparing their sorted properties
    # (see VariantWheel.__lt__())
    wheels.sort()


Integration with ``pylock.toml``
--------------------------------

The following section is added to the
:doc:`packaging:specifications/pylock-toml`:

.. code:: rst

    .. _pylock-packages-variants-json:

    ``[packages.variants-json]``
    ----------------------------

    - **Type**: table
    - **Required?**: no; requires that :ref:`pylock-packages-wheels` is used,
     mutually-exclusive with :ref:`pylock-packages-vcs`,
     :ref:`pylock-packages-directory`, and :ref:`pylock-packages-archive`.
    - **Inspiration**: uv_
    - The URL or path to the ``variants.json`` file.
    - Only used if the project uses :ref:`wheel variants <wheel-variants>`.

    .. _pylock-packages-variants-json-url:

    ``packages.variants-json.url``
    ''''''''''''''''''''''''''''''

    See :ref:`pylock-packages-archive-url`.

    .. _pylock-packages-variants-json-path:

    ``packages.variants-json.path``
    '''''''''''''''''''''''''''''''

    See :ref:`pylock-packages-archive-path`.

    .. _pylock-packages-variants-json-hashes:

    ``packages.variants-json.hashes``
    '''''''''''''''''''''''''''''''''

    See :ref:`pylock-packages-archive-hashes`.

If there is a ``[packages.variants-json]`` section, the installer SHOULD
resolve variants to select the best wheel file.


Provider plugin API
-------------------

High level design
'''''''''''''''''

Every provider plugin MUST operate within a single namespace. This
namespace is used as a unique key for all plugin-related operations. All
the properties defined by the plugin are bound within the plugin's
namespace, and the plugin defines all the valid feature names and values
within that namespace.

Provider plugin authors SHOULD choose namespaces that can be clearly
associated with the project they represent, and avoid namespaces that
refer to other projects or generic terms that could lead to naming
conflicts in the future.

All variants published on a single index for a specific package version
MUST use the same provider for a given namespace. Attempting to load
more than one plugin for the same namespace in the same release version
MUST result in a fatal error. While multiple plugins for the same
namespace MAY exist across different packages or release versions (such
as when a plugin is forked due to being unmaintained), they are mutually
exclusive within any single release version.

To make it easier to discover and install plugins, they SHOULD be
published in the same indexes that the packages using them. In
particular, packages published to PyPI MUST NOT rely on plugins that
need to be installed from other indexes.

Except for namespaces reserved as part of this PEP, installable Python
packages MUST be provided for plugins. However, as noted in the
`Providers`_ section, these plugins can also be reimplemented by tools
needing them. In the latter case, the resulting reimplementation does
not need to follow the API defined in this section.

Plugin packages may be run in an isolated environment. They MUST NOT
make decisions based on installed packages.

A plugin implemented as Python package exposes two kinds of objects at a
specified API endpoint:

a. attributes that return a specific value after being accessed via:

   .. code:: text

       {API endpoint}.{attribute name}

b. callables that are called via:

   .. code:: text

       {API endpoint}.{callable name}({arguments}...)

These can be implemented either as modules, or classes with class
methods or static methods. The specifics are provided in the subsequent
sections.


API endpoint
''''''''''''

The location of the plugin code is called an "API endpoint", and it is
expressed using the object reference notation following the
:doc:`packaging:specifications/entry-points`:

.. code:: text

    {import_path}(:{object_path})?

An API endpoint specification is equivalent to the following Python
pseudocode:

.. code:: python

    import {import_path}

    if "{object_path}":
        plugin = {import_path}.{object_path}
    else:
        plugin = {import_path}

API endpoints are used in two contexts:

a. in the ``plugin-api`` key of variant metadata, either explicitly or
   inferred from the package name in the ``requires`` key. This is the
   primary method of using the plugin when building and installing
   wheels.

b. as the value of an installed entry point in the ``variant_plugins``
   group. The name of said entry point is insignificant. This is
   OPTIONAL but RECOMMENDED, as it permits variant-related utilities to
   discover variant plugins installed to the user's environment.


Variant feature config class
''''''''''''''''''''''''''''

The variant feature config class is used as a return value in plugin API
functions. It defines a single variant feature, along with a list of
possible values. Depending on the context, the order of values MAY be
significant. It is defined using the following protocol:

.. code:: python

    from abc import abstractmethod
    from typing import Protocol


    class VariantFeatureConfigType(Protocol):
        @property
        @abstractmethod
        def name(self) -> str:
            """Feature name"""
            raise NotImplementedError

        @property
        @abstractmethod
        def multi_value(self) -> bool:
            """Does this property allow multiple values per variant?"""
            raise NotImplementedError

        @property
        @abstractmethod
        def values(self) -> list[str]:
            """List of values, possibly ordered from most preferred to least"""
            raise NotImplementedError

A "variant feature config" MUST provide the following properties or
attributes:

- ``name: str`` specifying the feature name.

- ``multi_value: bool`` specifying whether the feature is allowed to
  have multiple corresponding values within a single variant wheel. If
  it is ``False``, then it is an error to specify multiple values for
  the feature.

- ``values: list[str]`` specifying feature values. In contexts where the
  order is significant, the values MUST be ordered from the most
  preferred to the least preferred.

All features are interpreted as being within the plugin's namespace.


Plugin interface
''''''''''''''''

The plugin interface MUST follow the following protocol:

.. code:: python

    from abc import abstractmethod
    from typing import Protocol


    class PluginType(Protocol):
        # Note: properties are used here for docstring purposes, these
        # must be actually implemented as attributes.

        @property
        @abstractmethod
        def namespace(self) -> str:
            """The provider namespace"""
            raise NotImplementedError

        @property
        def is_aot_plugin(self) -> bool:
            """Is this plugin valid for `install-time = false`?"""
            return False

        @classmethod
        @abstractmethod
        def get_all_configs(cls) -> list[VariantFeatureConfigType]:
            """Get all valid configs for the plugin"""
            raise NotImplementedError

        @classmethod
        @abstractmethod
        def get_supported_configs(cls) -> list[VariantFeatureConfigType]:
            """Get supported configs for the current system"""
            raise NotImplementedError

The plugin interface MUST define the following attributes:

- ``namespace: str`` specifying the plugin's namespace.

- ``is_aot_plugin: bool`` indicating whether the plugin is a valid AoT
  plugin. If that is the case, ``get_supported_configs()`` MUST always
  return the same value as ``get_all_configs()`` (modulo ordering),
  which MUST be a fixed list independent of the platform on which the
  plugin is running. Defaults to ``False`` if unspecified.

The plugin interface MUST provide the following functions:

- ``get_all_config() -> list[VariantFeatureConfigType]`` that returns a
  list of "variant feature configs" describing all valid variant
  features within the plugin's namespace, along with all their permitted
  values. The ordering of the lists is insignificant here. A particular
  plugin version MUST always return the same value (modulo ordering),
  irrespective of any runtime conditions.

- ``get_supported_configs() -> list[VariantFeatureConfigType]`` that
  returns a list of "variant feature configs" describing the variant
  features within the plugin's namespace that are compatible with this
  particular system, along with their values that are supported. The
  variant feature and value lists MUST be ordered from the most
  preferred to the least preferred, as they affect `variant
  ordering`_.

The value returned by ``get_supported_configs()`` MUST be a subset of
the feature names and values returned by ``get_all_configs()`` (modulo
ordering).

The value returned by ``get_supported_configs()`` MAY be cached
throughout multiple packages in a single install session.


Example implementation
''''''''''''''''''''''

.. code:: python

    from dataclasses import dataclass


    @dataclass
    class VariantFeatureConfig:
        name: str
        values: list[str]
        multi_value: bool


    # internal -- provided for illustrative purpose
    _MAX_VERSION = 4
    _ALL_GPUS = ["narf", "poit", "zort"]


    def _get_current_version() -> int:
        """Returns currently installed runtime version"""
        ...  # implementation not provided


    def _is_gpu_available(codename: str) -> bool:
        """Is specified GPU installed?"""
        ...  # implementation not provided


    class MyPlugin:
        namespace = "example"

        # optional, defaults to False
        is_aot_plugin = False

        # all valid properties
        @staticmethod
        def get_all_configs() -> list[VariantFeatureConfig]:
            return [
                VariantFeatureConfig(
                    # example :: gpu -- multi-valued, since the package
                    # can target multiple GPUs
                    name="gpu",
                    # [narf, poit, zort]
                    values=_ALL_GPUS,
                    multi_value=True,
                ),
                VariantFeatureConfig(
                    # example :: min_version -- single-valued, since
                    # there is always one minimum
                    name="min_version",
                    # [1, 2, 3, 4] (order doesn't matter)
                    values=[str(x) for x in range(1, _MAX_VERSION + 1)],
                    multi_value=False,
                ),
            ]

        # properties compatible with the system
        @staticmethod
        def get_supported_configs() -> list[VariantFeatureConfig]:
            current_version = _get_current_version()
            if current_version is None:
                # no runtime found, system not supported at all
                return []

            return [
                VariantFeatureConfig(
                    name="min_version",
                    # [current, current - 1, ..., 1]
                    values=[str(x) for x in range(current_version, 0, -1)],
                    multi_value=False,
                ),
                VariantFeatureConfig(
                    name="gpu",
                    # this may be empty if no GPUs are supported --
                    # 'example :: gpu feature' is not supported then;
                    # but wheels with no GPU-specific code and only
                    # 'example :: min_version' could still be installed
                    values=[x for x in _ALL_GPUS if _is_gpu_available(x)],
                    multi_value=True,
                ),
            ]


Future extensions
'''''''''''''''''

The future versions of this specification, as well as third-party
extensions MAY introduce additional properties and methods on the plugin
instances. The implementations SHOULD ignore additional attributes.

For best compatibility, all private attributes SHOULD be prefixed with
an underscore (``_``) character to avoid incidental conflicts with
future extensions.


Build backends
--------------

As a build backend can't determine whether the frontend supports variant
wheels or not, :pep:`517` and :pep:`660` hooks MUST build non-variant
wheels by default. Build backends MAY provide ways to request variant
builds. This specification does not define any specific configuration.

When building variant wheels, build backends MUST verify variant
metadata for correctness, and they MUST NOT emit wheels with
nonconformant ``variant.json`` files. They SHOULD also query providers
to determine whether variant properties requested by the user are valid,
though they MAY permit skipping this verification and therefore emitting
variant wheels with potentially unknown properties.


Variant environment markers
---------------------------

Four new :ref:`environment markers
<dependency-specifiers-environment-markers>` are introduced in
dependency specifications:

1. ``variant_namespaces`` corresponding to the set of namespaces of all
   the variant properties that the wheel variant was built for.
2. ``variant_features`` corresponding to the set of
   ``namespace :: feature`` pairs of all the variant properties that the
   wheel variant was built for.
3. ``variant_properties`` corresponding to the set of
   ``namespace :: feature :: value`` tuples of all the variant
   properties that the wheel variant was built for.
4. ``variant_label`` corresponding to the exact variant label that the
   wheel was built with. For the non-variant wheel, it is an empty
   string.

The markers evaluating to sets of strings MUST be matched via the ``in``
or ``not in`` operator, e.g.:

.. code::

    # satisfied by any "foo :: * :: *" property
    dep1; "foo" in variant_namespaces
    # satisfied by any "foo :: bar :: *" property
    dep2; "foo :: bar" in variant_features
    # satisfied only by "foo :: bar :: baz" property
    dep3; "foo :: bar :: baz" in variant_properties

The ``variant_label`` marker is a plain string:

.. code::

    # satisfied by the variant "foobar"
    dep4; variant_label == "foobar"
    # satisfied by any wheel other other than the null variant
    # (including the non-variant wheel)
    dep5; variant_label != "null"
    # satisfied by the non-variant wheel
    dep6; variant_label == ""

Implementations MUST ignore differences in whitespace while matching the
features and properties.

Variant marker expressions MUST be evaluated against the variant
properties stored in the wheel being installed, not against the current
output of the provider plugins. If a non-variant wheel was selected or
built, all variant markers evaluate to ``False``.


ABI Dependency Variant Namespace (Optional)
-------------------------------------------

This section describes an **OPTIONAL** extension to the wheel variant
specification. Tools that choose to implement this feature MUST follow
this specification. Tools that do not implement this feature MUST treat
the variants using it as incompatible, and SHOULD inform users when such
wheels are skipped.

The variant namespace ``abi_dependency`` is reserved for expressing that
different builds of the same version of a package are compatible with
different versions or version ranges of a dependency. This namespace
MUST NOT be used by any variant provider plugin, it MUST NOT be listed
in ``providers`` metadata, and can only appear in a built wheel variant
property.

Within this namespace, zero or more properties can be used to express
compatible dependency versions. For each property, the feature name MUST
be the :ref:`normalized name <packaging:name-normalization>` of the
dependency, whereas the value MUST be a valid release segment of
a public version identifier, as defined by the
:doc:`packaging:specifications/version-specifiers` specification.
It MUST contain up to three version components, that are matched against
the installed version same as the ``=={value}.*`` specifier. Notably,
trailing zeroes match versions with fewer components (e.g. ``2.0``
matches release ``2`` but not ``2.1``). This also implies that the
property values have different semantics than PEP 440 versions, in
particular ``2``, ``2.0`` and ``2.0.0`` represent different ranges.

Versions with nonzero epoch are not supported.

==================================== ==================
Variant Property                     Matching Rule
==================================== ==================
``abi_dependency :: torch :: 2``     ``torch==2.*``
``abi_dependency :: torch :: 2.9``   ``torch==2.9.*``
``abi_dependency :: torch :: 2.8.0`` ``torch==2.8.0.*``
==================================== ==================

Multiple variant properties with the same feature name can be used to
indicate wheels compatible with multiple providing package versions,
e.g.:

.. code:: text

    abi_dependency :: torch :: 2.8.0
    abi_dependency :: torch :: 2.9.0

This means the wheel is compatible with both PyTorch 2.8.0 and 2.9.0.


How to teach this
=================

Python package users
--------------------

The primary source of information for Python package users should be
installer documentation, supplemented by helpful informational messages
from command-line interface, and tutorials. Users without special needs
should not require any special variant awareness. Advanced users would
specifically need documentation on (provided the installer in question
implements these features):

- enabling untrusted provider plugins and the security implications of
  that

- controlling provider usage, in particular enabling optional providers,
  disabling undesirable plugins or disabling variant usage in general

- explicitly selecting variants, as well as controlling variant
  selection process

- configuring variant selection for remote deployment targets, for
  example using a static file generated on the target

The installer documentation may also be supplemented by documentation
specific to Python projects, in particular their installation
instructions.

For the transition period, during which some package managers do and
some do not support variant wheels, users need to be aware that certain
features may only be available with certain tools.


Python package maintainers
--------------------------

The primary source of information for maintainers of Python packages
should be build backend documentation, supplemented by tutorials. The
documentation needs to indicate:

- how to declare variant support in ``pyproject.toml``

- how to use variant environment markers to specify dependencies

- how to build variant wheels

- how to publish them and generate the ``*-variants.json`` file on local
  indexes

The maintainers will also need to peruse provider plugin documentation.
They should also be aware which provider plugins are considered trusted
by commonly used installers, and know the implications of using
untrusted plugins. These materials may also be supplemented by generic
documents explaining publishing variant wheels, along with specific
example use cases.

For the transition period, package maintainers need to be aware that
they should still publish non-variant wheels for backwards
compatibility.


Backwards compatibility
=======================

Existing installers MUST NOT accidentally install variant wheels, as
they require additional logic to determine whether a wheel is compatible
with the user's system. This is achieved by `extending wheel filename
<#extended-wheel-filename>`__ through adding a ``-{variant label}``
component to the end of the filename, effectively causing variant wheels
to be rejected by common installer implementations. For backwards
compatibility, a non-variant wheel can be published in addition to the
variant wheels. It will be the only wheel supported by incompatible
installers, and the least preferred wheel for variant-compatible
installers.

Aside from this explicit incompatibility, the specification makes
minimal and non-intrusive changes to the binary package format. The
`variant metadata`_ is placed in a separate file in the ``.dist-info``
directory, which should be preserved by tools that are not concerned
with variants, limiting the necessary changes to updating the filename
validation algorithm (if there is one).

If the new `variant environment markers`_ are used in wheel
dependencies, these wheels will be incompatible with existing tools.
This is a general problem with the design of environment markers, and
not specific to wheel variants. It is possible to work around this
problem by partially evaluating environment markers at build time, and
removing the markers or dependencies specific to variant wheels from the
non-variant wheel.

`Build backends`_ produce non-variant wheels to preserve backwards
compatibility with existing frontends. Variant wheels can only be output
on explicit user request.

By using a separate ``*-variants.json`` `file for shared metadata
<#name-version-variants-json-the-index-level-variant-metadata-file>`__,
it is
possible to use variant wheels on an index that does not specifically
support variant metadata. However, the index MUST permit distributing
wheels that use the extended filename syntax and the JSON file.


Reference implementation
========================

The `variantlib <https://github.com/wheelnext/variantlib>`__ project
contains a reference implementation of all the protocols and algorithms
introduced in this PEP, as well as a command-line tool to convert
wheels, generate the ``*-variants.json`` index and query plugins.

A client for installing variant wheels is implemented in a
`uv branch <https://github.com/astral-sh/uv/pull/12203>`__.

The `Wheel Variants monorepo
<https://github.com/wheelnext/pep_xxx_wheel_variants>`__ includes
example implementations of provider plugins, as well as modified
versions of build backends featuring variant wheel building support and
modified versions of some Python packages demonstrating variant wheel
uses.


Rejected ideas
==============

Variants being entirely or transitionally opt-in
------------------------------------------------

In discussing the security concerns, proposals were made to make variant
provider usage entirely opt-in, either permanently, or at least
initially to facilitate further testing. While such approaches may
alter who takes responsibility of vetting the provider code, and hence
the maintenance effort or number of packages/maintainers that need to be
trusted, they are not suitable as long-term solutions.

Most importantly, the opt-in mechanism would lead to far worse user
experience out-of-the-box. For variant-enabled packages, the default
experience would be installing a suboptimal or outright broken variant.
It should be noted that variant-enabled packages may not only be
installed directly, but also as dependencies of other packages.
Therefore, for optimal user experience, all packages that are
variant-enabled or that feature dependencies that are variant-enabled,
would have to document appropriate installer-specific mechanisms for
enabling the respective provider plugins.

The proliferation of this experience could have two significant
outcomes. The inability to make variants work out of the box for users
could lead to package maintainers refraining from using them, and
instead sticking to the earlier workarounds. What's even worse, it could
also lead to users eventually naively configuring their installers
to enable all variant providers unconditionally, effectively rendering
the provider usage opt-out for a large number of users, enabling all
kinds of supply chain attacks described in the `security implications`_
section.

The authors would like to emphasize that security cannot be achieved at
the cost of severely impaired user experience. Instead, the PEP attempts
to strike a balance by introducing a centrally maintained and vetted
pool of trusted providers.


An approach without provider plugins
------------------------------------

The support for additional variant properties could technically be
implemented without introducing provider plugins, but rather defining
the available properties and their discovery methods as part of the
specification, much like how wheel tags are implemented currently.
However, the existing wheel tag logic already imposes a significant
complexity on packaging tools that need to maintain the logic for
generating supported tags, partially amortized by the data provided by
the Python interpreter itself.

Every new axis would be imposing even more effort on package manager
maintainers, who would have to maintain an algorithm to determine the
property compatibility. This algorithm could become quite complex,
possibly needing to account for different platforms, hardware versions
and requiring more frequent updates than the one for platform tags. This
would also significantly increase the barrier towards adding new axes
and therefore the risk of lack of feature parity between different
installers, as every new axis will be imposing additional maintenance
cost.

For comparison, the plugin design essentially democratizes the variant
properties. Provider plugins can be maintained independently by people
having the necessary knowledge and hardware. They can be updated as
frequently as necessary, independently of package managers. The decision
to use a particular provider falls entirely on the maintainer of package
needing it, though they need to take into consideration that using
plugins that are not vetted by the common installers will inconvenience
their users.


Resolving variants to separate packages
---------------------------------------

An alternative proposal was to publish the variants of the package as
separate projects on the index, along with the main package serving as a
"resolver" directing to other variants via its metadata. For example, a
``torch`` package could indicate the conditions for using ``torch-cpu``,
``torch-cu129``, etc. subpackages.

Such an approach could possibly feature better backwards compatibility
with existing tools. The changes would be limited to installers, and
even with pre-variant installers the users could explicitly request
installing a specific variant. However, it poses problems at multiple
levels.

The necessity of creating a new project for every variant will lead to
the proliferation of old projects, such as ``torch-cu123``. While the
use of resolver package will ensure that only the modern variants are
used, users manually installing packages and cross-package dependencies
may accidentally be pinning to old variant projects, or even fall victim
to name squatting. For comparison, the variant wheel proposal scopes
variants to each project version, and ensures that only the project
maintainers can upload them.

Furthermore, it requires significant changes to the dependency resolver
and package metadata formats. In particular, the dependency resolver
would need to query all "resolver" packages before performing
resolution. It is unclear how to account for such variants while
performing universal resolution. The one-to-one mapping between
dependencies and installed packages would be lost, as a ``torch``
dependency could effectively be satisfied by ``torch-cu129``.


Appendices
==========

- :ref:`0817-variant-json-schema`


References
==========

.. _PyTorch: https://pytorch.org/
.. _JAX: https://docs.jax.dev/en/latest/
.. _NumPy: https://numpy.org/
.. _SciPy: https://scipy.org/
.. _GROMACS: https://www.gromacs.org/
.. _CuPy: https://cupy.dev/
.. _Spyder IDE: https://www.spyder-ide.org/
.. _XGBoost: https://xgboost.ai/
.. _vLLM: https://docs.vllm.ai/en/latest/index.html
.. _SLEAP: https://sleap.ai/


Acknowledgements
================

This work would not have been possible without the contributions and
feedback of many people in the Python packaging community. In
particular, we would like to credit the following individuals for their
help in shaping this PEP (in alphabetical order):

Alban Desmaison, Bradley Dice, Chris Gottbrath, Dmitry Rogozhkin,
Emma Smith, Geoffrey Thomas, Henry Schreiner, Jeff Daily, Jeremy Tanner,
Jithun Nair, Keith Kraus, Leo Fang, Mike McCarty, Nikita Shulga,
Paul Ganssle, Philip Hyunsu Cho, Robert Maynard, Vyas Ramasubramani,
and Zanie Blue.


Change history
==============

- 18-Mar-2026

  - Added high-level outlines of suggested implementation logic per type
    of packaging tool, and a diagram for installer behavior.
  - Deemphasized vendoring providers in installers. While it is still
    permitted as an implementation choice, it is not presented as the
    recommended solution to improve security anymore.
  - Made a centrally maintained allowlist the primary solution for
    enabling providers by default. Such an allowlist would be maintained
    by a dedicated team, starting with a subset of the PEP authors.
  - Clarified the specification to permit using user-provided
    compatibility information in place of provider queries.
  - Removed unnecessary UX suggestions regarding the opt-in mechanism.
  - Clarified that the index level variant metadata file can be
    generated by the index itself, or uploaded by the package maintainer
    if index does not support that.
  - Added a recommendation that no new variants are introduced once the
    index level variant metadata file is published.
  - Added an explicit recommendation that variant provider packages are
    run in an isolated environment.
  - Clarified that the value returned by ``get_supported_configs()`` may
    be cached.
  - Emphasized the risks of a full scale opt-in approach.
  - Updated the GROMACS plot to respect dark theme.


Copyright
=========

This document is placed in the public domain or under the
CC0-1.0-Universal license, whichever is more permissive.
