PEP: 825
Title: Wheel Variants: Package Format
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>
PEP-Delegate: Paul Moore <p.f.moore@gmail.com>
Discussions-To: https://discuss.python.org/t/pep-817-split-wheel-variants-package-format/106196
Status: Draft
Type: Standards Track
Topic: Packaging
Created: 17-Feb-2026
Post-History: `17-Feb-2026 <https://discuss.python.org/t/pep-817-split-wheel-variants-package-format/106196>`__

Abstract
========

This PEP proposes variant wheels, an extension to
:doc:`packaging:specifications/binary-distribution-format` that permits
building multiple variants of the same package while embedding
additional compatibility data. The specific properties are stored inside
the wheel, and expressed via a human-readable variant label in the
filename, which is then mapped to the actual properties via a separately
hosted JSON mapping. This aims to make ``{tool} install {package}``
capable of selecting the most appropriate variant of packages where
additional compatibility dimensions such as GPU support need to be
accounted for.


Motivation
==========

This PEP proposes a protocol to record additional compatibility data in
binary packages, to allow tools to pick the correct package to use in
situations where
:doc:`packaging:specifications/platform-compatibility-tags` are
insufficient. There are many cases where this is necessary, most notably
in the case of scientific and machine learning (ML) libraries, where
high performance requires extension code that is carefully tailored to
the precise hardware available in the user's environment. Well known
examples of this include:

- PyTorch and other ML tools which depend on the user's GPU hardware and
  driver.
- Scientific libraries like SciPy, which can be linked to different
  linear algebra libraries.
- Libraries such as XGBoost that can be linked to different OpenMP
  runtimes.
- Libraries that ship performance enhanced builds which can be used when
  certain CPU instruction sets are available, such as AVX2 or AVX-512.

The problem space has been explored in greater detail in :pep:`817`.


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`.


Variant wheel
-------------

A variant wheel is an extension of the wheel format, defined in
:doc:`packaging:specifications/binary-distribution-format`. It
MUST specify a `variant label`_ in the filename, which makes it
distinct from non-variant wheels. It MUST include a
`variant metadata`_ file, which maps the variant label to zero or
more `variant properties`_.

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

Variant properties express the compatibility of binary packages with
specific platforms, in addition to
:doc:`packaging:specifications/platform-compatibility-tags`. They follow
a key-value format, where a key is called a *variant feature*. The
keys are further grouped into independently governed *variant
namespaces*. Hence, a variant feature consists of a namespace and a
feature name, whereas a variant property consists of a namespace, a
feature name and a feature value.

Variant properties are serialized into a structured 3-tuple of the
following format::

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

The properties with which the wheel was built are stored within the
wheel, in the `variant metadata`_ file. A variant wheel can specify
multiple values corresponding to a variant feature. For the wheel to be
considered compatible with a system, at least one value for every
feature listed in its properties MUST be compatible with the system.
A variant wheel with zero properties is always deemed compatible.

The namespace and feature name components MUST be non-empty and consist
only of ``0-9``, ``a-z`` and ``_`` ASCII characters (``^[a-z0-9_]+$``).
The feature value MUST be non-empty and consist only of ``0-9``,
``a-z``, ``_`` and ``.`` ASCII Characters (``^[a-z0-9_.]+$``).

The available properties and the rules governing their compatibility
will be defined in a subsequent PEP.

Examples:

.. code:: text

    # the system must be compatible with all of the following
    x86_64 :: level :: v3
    x86_64 :: avx512_bf16 :: on
    nvidia :: cuda_version_lower_bound :: 12.8
    # it must also be compatible with at least one of the following
    nvidia :: sm_arch :: 120_real
    nvidia :: sm_arch :: 110_real


Variant label
-------------

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
                                                                                 +++++++++++++++++++

The Python tag component MUST NOT start with a digit.

Variant wheels MUST include the variant label component. Conversely,
wheels without variant label are non-variant wheels. The variant label
MUST be non-empty and consist only of ``0-9``, ``a-z``, ``_`` and ``.``
ASCII characters (``^[0-9a-z_.]+$``).

Every variant 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.

The label ``null`` is reserved and always corresponds to the variant
with zero properties, called a null variant. This variant acts as a
fallback variant that is always compatible.

Examples:

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


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

The additional metadata specific to variant wheels is stored inside the
wheel, in ``*.dist-info/variant.json`` file, using the JSON format. This
PEP defines the following structure:

.. code:: text

    +- $schema
    +- default-priorities
    |  +- namespace        : list[str]
    |  +- feature
    |     +- {namespace}   : list[str]  = []
    |  +- property
    |     +- {namespace}
    |        +- {feature}  : list[str]  = []
    +- variants
      +- {variant_label}
         +- {namespace}
            +- {feature}   : list[str]  = []

This structure corresponds to the version ``0.1.0`` of the format. The
version number is stored as part of the schema_ URL. Whenever the format
changes, the version number must be incremented. Tools MUST assume that
all variant wheels using an unknown format version are unsupported.

The version numbers starting with zero are reserved for drafts and MUST
NOT be used in production. Once the proposal is complete, the latest
draft will be promoted to version ``1.0.0``.

The top-level keys are described in the subsequent sections.


Schema
''''''

The ``$schema`` key is the standard way of specifying the `JSON schema
<https://json-schema.org/>`__ used. Its value MUST be the URL of a JSON
schema corresponding to this specification, hosted on
``packaging.python.org``. The schema URL MUST include a version number,
and consequently every schema MUST describe the matching format version.
The schema can be used to verify the validity of the JSON file prior to
processing it, or after outputting it.

A proposed JSON schema for the current format version is included in the
Appendix of this PEP. Subsequent PEPs changing the metadata format will
include updated versions of the schema. The schema is available in
:ref:`pep825-variant-json-schema`.


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

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

The following key is REQUIRED:

- ``namespace: list[str]``: All variant namespaces used in variant
  wheels for a given package version, ordered in decreasing priority.
  This list MUST contain all namespaces used in variant properties.

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 feature names are ordered in decreasing priority. It is used to
  override the default feature ordering.

- ``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 feature values are ordered in decreasing
  priority. It is used to override the default value ordering.


Variants
''''''''

The ``variants`` dictionary provides a mapping from variant labels
to variant properties. In the variant wheel, it MUST contain the label
present in that wheel's filename.

It has 3 levels. 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 sets of feature values, converted to lists,
sorted lexically.


Example
'''''''

.. code:: json5

    {
      // The schema URL will be replaced with the final URL on packaging.python.org
      "$schema": "https://variants-schema.wheelnext.dev/peps/825/v0.1.0.json",

      "default-priorities": {
        // REQUIRED: specifies that x86_64 CPU properties are more important than
        // aarch64 CPU properties (both are mutually exclusive, so the exact order
        // does not matter), and both are more important than specific BLAS/LAPACK
        // library:
        "namespace": ["x86_64", "aarch64", "blas_lapack"],

        // OPTIONAL: makes "library" the most important feature in "blas_lapack"
        // namespace
        "feature": {
          "blas_lapack": ["library"]
        },

        // OPTIONAL: makes ["mkl", "openblas"] the most important values of
        // "blas_lapack :: library" feature
        "property": {
          "blas_lapack": {
            "library": ["mkl", "openblas"]
          }
        }
      },

      "variants": {
        // REQUIRED: in variant.json, always a single entry, with the key
        // matching the variant label ("x86_64_v3_openblas") and the value
        // specifying its properties (the system must be compatible with both):
        // - blas_lapack :: library :: openblas
        // - x86_64 :: level :: v3
        "x86_64_v3_openblas": {
          "blas_lapack": {
            "library": ["openblas"]
          },
          "x86_64": {
            "level": ["v3"]
          }
        }
      }
    }


Index-level metadata
--------------------

For every package version that includes at least one variant wheel,
there MUST exist a corresponding ``{name}-{version}-variants.json``
file. 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 exact URL where the file is hosted is insignificant, but it MUST
be provided in all the responses where the variant wheels are included.
It should follow the rules for files in the
:ref:`packaging:simple-repository-api`, except that the optional
metadata attributes served by the index (such as ``core-metadata``,
``dist-info-metadata``, ``requires-python`` or ``yanked``) are not
meaningful for that file. Indexes MAY publish or skip these attributes,
as long as the values do not prevent correct operation. Tools MAY either
use or ignore these values.

This file uses the same structure as `variant metadata`_, except that
the ``variants`` object MUST list all variants available on the package
index for the package version in question. The tools MUST ensure that
the variant metadata across multiple variant wheels of the same package
version and the index-level metadata file is consistent. They MAY
require that keys other than ``variants`` have exactly the same values,
or they may carefully merge their values, provided that no conflicting
information is introduced, and the result is the same irrespective of
the order in which the wheels are processed.

This file SHOULD NOT be considered immutable and MAY be updated in a
backward compatible way at any point (e.g. when adding a new variant).

Variant indexes MAY elect to either auto-generate the file from the
uploaded variant wheels or allow the user to manually generate it
themselves and upload it to the index.

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/peps/825/v0.1.0.json",
      "default-priorities": {
        // identical to above
      },
      "variants": {
        // REQUIRED: entries for all wheel variants for the package version

        // if a null variant is present
        "null": {},

        // "x86_64_v3_openblas" label corresponds to:
        // - blas_lapack :: library :: openblas
        // - x86_64 :: level :: v3
        "x86_64_v3_openblas": {
          "blas_lapack": {
            "library": ["openblas"]
          },
          "x86_64": {
            "level": ["v3"]
          }
        },

        // "x86_64_v4_mkl" label corresponds to:
        // - blas_lapack :: library :: mkl
        // - x86_64 :: level :: v4
        "x86_64_v4_mkl": {
          "blas_lapack": {
            "library": ["mkl"]
          },
          "x86_64": {
            "level": ["v4"]
          }
        }
      }
    }


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

This specification defines an ordering between different wheels based on
the presence of variant metadata.

For the purpose of ordering, variant properties are grouped into
features, and features into namespaces. For every namespace, the tool
MUST obtain a list of compatible features, and for every feature, a list
of compatible values. The method of obtaining these lists will be
defined in a subsequent PEP. The items in these lists will be provided
in specific order that will impact variant wheel ordering.

The compatible wheels corresponding to a particular combination of
package name, version and build number MUST be grouped by their variant
label, and a separate group of non-variant wheels MUST be formed. The
groups of variant wheels MUST then be ordered according to the following
algorithm:

1. Construct the ordered list of namespaces by copying the value of the
   ``default-priorities.namespace`` key from `index-level metadata`_.
   This is ``namespace_order`` in the example.

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. Take the ordered list of compatible feature names obtained
       previously and iterate over it, 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. This is ``feature_order`` in the example.

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. Take the ordered list of compatible feature values obtained
       previously and iterate over it, 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. This is ``value_order`` in the example.

4. For every group, determine the most preferred value corresponding to
   every variant feature present in the variant properties corresponding
   to the group. This is done by finding among the values the one that
   has the lowest position in the ordered property value list. After
   this step, a list of features along with their best values is
   available for every variant. This is done in the
   ``VariantWheel.best_value_properties()`` method in the example.

5. For every item in the list constructed in the previous step,
   construct a sort key that is a 3-tuple consisting of
   its namespace, feature name and best feature value indices in the
   respective ordered lists. This is done by the ``property_key()``
   function in the example.

6. For every group, sort the list constructed in step 4 using the sort
   keys constructed in step 5, in ascending order. This is done by the
   ``VariantWheel.sorted_properties()`` method in the example.

7. To order groups, compare their sorted lists from step 6. If the
   sort keys at the first position are different, the group with the
   lower key is sorted earlier. If they are the same, compare the keys
   at the second position, and so on, until either a tie-breaker is
   found or the list in one of the groups is exhausted. In the latter
   case, the group with more keys is sorted earlier. As a fallback,
   if both groups have the same number of keys, they are ordered
   lexically by the variant label, ascending. This is done by the
   ultimate step of the example algorithm, with the comparison function
   being implemented as ``VariantWheel.__lt__()``.

The algorithm sorts the group of null variant wheels last, as they
feature no variant properties. The group of non-variant wheels MUST be
placed after all the other groups.

Within every group, the wheels MUST then be ordered according to their
platform compatibility tags. After this process, the variant wheels are
sorted from the most preferred to the least preferred.

The tools MAY provide options to override the default ordering, for
example by specifying a preference for specific namespaces, features
or properties. The tools MAY also provide options to exclude specific
variants, or to select a particular variant.

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 the subsequent ordering by platform
compatibility tags.

.. code:: python

    from typing import Self


    def get_compatible_feature_names(namespace: str) -> list[str]:
        """Get an ordered list of compatible features"""
        ...


    def get_compatible_feature_values(namespace: str, feature_name: str) -> list[str]:
        """Get an ordered list of compatible values"""
        ...


    # default-priorities dict from index-level 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_compatible_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_compatible_feature_values(namespace, feature_name):
                if feature_value not in value_order[namespace][feature_name]:
                    value_order[namespace][feature_name].append(feature_value)


    def best_value_property(namespace: str, feature_name: str, feature_values: str) -> str:
        """Helper function to determine the best value for given feature"""
        for best_value in value_order[namespace][feature_name]:
            if best_value in feature_values:
                return best_value
        assert False, "No feature value supported, wheel should have been filtered out"


    def property_key(prop: tuple[str, str, str]) -> tuple[int, int, int]:
        """Construct a sort key for variant property (akin to step 5.)"""
        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"""

        label: str
        # {namespace: {feature_name: [feature_values]}}, as in variant.json
        properties: dict[str, dict[str, list[str]]]

        def best_value_properties(self: Self) -> list[tuple[str, str, str]]:
            """Determine the most preferred values for every feature, step 4."""
            return [
                (
                    namespace,
                    feature_name,
                    best_value_property(namespace, feature_name, feature_values),
                )
                for namespace, features in self.properties.items()
                for feature_name, feature_values in features.items()
            ]

        def sorted_properties(self: Self) -> list[tuple[str, str, str]]:
            """Sort the list of features with their best values (step 6.)"""
            return sorted(self.best_value_properties(), key=property_key)

        def __lt__(self: Self, other: Self) -> bool:
            """Variant comparison function for sorting (part of step 7.)"""
            self_properties = self.sorted_properties()
            other_properties = other.sorted_properties()
            # Proceed from the first to the last common sort best-value property.
            # If any of them are different, the variant with better property wins.
            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)
            # If the best-value properties of one variant are a subset of another,
            # the one with more properties wins.
            if len(self_properties) != len(other_properties):
                return len(self_properties) > len(other_properties)
            # If two variants have exactly the same properties, fall back to
            # sorting on variant label (they must be unique).
            return self.label < other.label


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


    # 7. Order variant wheels by comparing their sorted properties
    # (see VariantWheel.__lt__())
    variant_wheels.sort()


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.


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

Variant wheels can be listed in ``pylock.toml`` file in the same manner
as wheels with different Platform compatibility tags: either all variant
(and non-variant) wheels can be listed, or a subset of them.

A new ``[packages.variants-json]`` subtable is added to the file. It
MUST inline the contents of the `index-level metadata`_ file, converting
the JSON structure into the respective TOML types. The ``$schema`` key
MUST be preserved to facilitate versioning. The tools MAY remove keys
that are not relevant to variant wheels present in ``pylock.toml``.

If variant wheels are listed, the tool SHOULD resolve variants to select
the best wheel file.


Proposed specification update
'''''''''''''''''''''''''''''

The proposed text for :doc:`packaging:specifications/pylock-toml`
follows:

``[packages.variant_json]``
~~~~~~~~~~~~~~~~~~~~~~~~~~~

- **Type**: table
- **Required?**: no
- **Functions**:

  - Inline variant selection metadata for the package.
  - The structure of this table MUST conform to the JSON Schema.
  - Tools that support variant-aware resolution MUST validate this table
    against the referenced schema.
  - Tools that do not support variant-aware resolution MAY ignore this table
    but SHOULD preserve it when rewriting the lock file.


Example
'''''''

.. code:: toml

    lock-version = "1.0"
    created-by = "uv"
    requires-python = ">=3.14"

    [[packages]]
    name = "numpy"
    version = "2.3.4"
    index = "https://pypi.anaconda.org/mgorny/simple"
    wheels = [
        { url = "https://pypi.anaconda.org/mgorny/simple/numpy/2.3.4/numpy-2.3.4-cp314-cp314-linux_x86_64-openblas.whl", hashes = {} },
        { url = "https://pypi.anaconda.org/mgorny/simple/numpy/2.3.4/numpy-2.3.4-cp314-cp314-linux_x86_64-x86_64_v4_mkl.whl", hashes = {} },
        { url = "https://pypi.anaconda.org/mgorny/simple/numpy/2.3.4/numpy-2.3.4-cp314-cp314-macosx_13_0_x86_64-accelerate.whl", hashes = {} },
        { url = "https://pypi.anaconda.org/mgorny/simple/numpy/2.3.4/numpy-2.3.4-cp314-cp314-macosx_13_0_x86_64-openblas.whl", hashes = {} },
    ]

    [packages.variant_json]
    "$schema" = "https://variants-schema.wheelnext.dev/peps/825/v0.1.0.json"

    [packages.variant_json.default-priorities]
    namespace = [ "x86_64", "aarch64", "blas_lapack" ]

    [packages.variant_json.variants]
    null = { }
    x86_64_v3_openblas = { "blas_lapack" = { "library" = ["openblas"]}, "x86_64" = { "level" = ["v3"]}  }
    x86_64_v4_mkl = { "blas_lapack" = { "library" = ["mkl"]}, "x86_64" = { "level" = ["v4"]}  }


Suggested implementation logic for tools (non-normative)
--------------------------------------------------------

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

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

1. Query the remote index for the package in question.
2. Initially select a package version meeting the version constraints
   (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 a `variant label`_, download the `index-level
   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 compatible variant properties. The
   mechanism for this will be specified in a subsequent PEP.
8. Filter and order variants based on the lists of compatible
   properties, per `variant ordering`_, 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.

Note that steps 4. through 8. are introduced specifically for variant
wheels. The remaining steps correspond to the current installer
behavior.

When installing from a source that does not provide an `index-level
metadata`_, the same algorithm can be used, except that the variant
metadata needs to be read directly from the wheels.


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

When asked to install a local wheel file, the 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 the `variant metadata`_ from ``*.dist-info/variant.json`` inside
   the wheel file.
4. Obtain the ordered lists of compatible variant properties. The
   mechanism for this will be specified in a subsequent PEP.
5. Verify the wheel compatibility via compatible properties.


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.

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 ``variants`` dictionary are merged 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


Rationale
=========

This PEP is part of a larger variant wheel design that was originally
proposed as :pep:`817`. However, due to its complexity, we decided to
split it into smaller parts that build one upon another. This PEP is the
first in the series, providing foundations including the file format
along with necessary metadata, index support and basic tool algorithms.
Aspects such as providing actual variant properties or building wheels
are deferred into subsequent PEPs.

Variant wheels use structured `variant properties`_ to express
multidimensional wheel compatibility matrices. Properties are organized
in namespaces that can be defined and governed independently. The
key-value structure makes the properties more flexible: adding a new
compatibility axis can be done by adding a new key. It can support both
AND-style dependencies (for example, a CPU plugin could define multiple
keys corresponding to different instructions sets, all of which are used
in the package and therefore must be supported) and OR-style
dependencies (for example, a GPU plugin can define a single key listing
multiple GPU types, indicating that all of them are supported by the
package, and therefore the users needs to own only one of them).

The specification does not impose any formal limits on the number of
properties expressed, and specifically accounts for the possibility of
property sets being very long (for example, a long list of GPUs or CPU
extension sets). To avoid wheel filenames becoming hard to comprehend
because of excess of information and potentially causing technical
issues because of their length, the property lists are stored inside
the wheel and mapped to a short label that is chosen by the package
maintainer and intended to be human-readable.

Wheel filenames alone do not provide sufficient metadata to drive
variant wheel selection. To avoid tools having to fetch the variant
metadata straight from multiple wheel files, the metadata from wheels
for every package version is combined and republished. This metadata is
scoped to a single package version to permit variants changing in the
future version.

The index support aims to account for three scenarios:

1. An index implementation that cannot embed additional metadata as part
   of file list responses. For example, this covers installing straight
   from a directory listing created by a webserver. To account for this
   scenario, index-level metadata is published as a plain JSON file that
   can be generated by the package maintainer and placed alongside
   wheels.

2. An index implementation that has more complete wheel support but does
   not wish to implement full variant wheel support immediately. The
   index needs only to permit the user to upload said JSON file. To
   account for minimalistic implementation, the specification permits
   the index to treat said file similarly to a wheel, including
   publishing attributes such as ``yanked``, as long as their values do
   not prevent clients from working.

3. An index implementation that implements complete wheel variant
   support. Such an index will parse uploaded variant wheels, and
   dynamically create the index-level metadata. The JSON file path would
   then be treated as an API endpoint rather than an actual file.

Since JSON format does not feature a set type, sets in the metadata are
represented as sorted lists. Sorting ensures reproducibility and makes
it possible to use equality comparison over whole dictionaries without
having to convert specific fields back to sets after deserialization.

The variant ordering algorithm has been proposed with the assumption
that variant properties take precedence over Platform compatibility
tags, as they are primarily used to express user preferences. This
accounts for possible divergence of platform tags, e.g. because a CUDA
variant may require a different minimal libc version, in which case the
selection should be driven by the desired CUDA preference rather than
incidental platform tag difference.

While a future PEP will define how variant properties are provided, a
baseline assumption is made that the compatible properties will be
provided in specific order corresponding to their preference. This makes
it possible to use a generic sorting algorithm, and later define
properties as data without having to change the algorithm.

A future PEP will define how the ordering for features and values is
provided. However, namespaces are governed independently and considered
on equal footing, and therefore there will be no standard ordering for
them. Instead, the ordering of namespaces will be explicitly stated in
the variants metadata, which in turn will be provided by the package
maintainer as part of the build process. For completeness, it will also
be possible to provide overrides for the ordering of features and values
via the same mechanism.

In the vast majority of real use cases, ordering based on properties
will suffice. However, in a pathological case two different variant
wheels may end up with equal sort keys. To provide reproducible results
in this case, fallback sorting on variant label is performed.

A concept of null variant is introduced that is distinct from
non-variant wheels to facilitate a transition period. This variant is
always supported by tools implementing this PEP, and takes precedence
over non-variant wheel. It can therefore be used to provide a distinct
fallback for the cases of no other variant being supported and variant
wheels being unsupported altogether. For example, PyTorch could provide
a much smaller null variant that is used when no GPU is supported, and a
fallback non-variant wheel built for the default CUDA version.

``pylock.toml`` integration inlines the variant metadata to keep the
file standalone. This can avoid the additional network call that would
be required to fetch the file, and avoids having to pin to a specific
hash that could cause problems if the file changed on the index, either
due to the variant metadata being updated or being generated in a way
that does not guarantee stable bytewise output.


Backwards Compatibility
=======================

Variant wheels add an additional `variant label`_ component to the wheel
filename. A complete filename verification step should reject such
wheels:

- 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, the filename is considered invalid. Example:

  .. code-block:: text

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

Currently, no Python tags start with a digit. To guarantee unambiguity,
the specification enforces that going forward. Tools commonly used to
install wheels at the time of writing implemented a verification
algorithm of that kind, making it possible to publish variant wheels on
an index alongside non-variant wheels without risk of them being
installed accidentally.

Tools that do not perform full filename verification will consume some
or all variant wheels as regular wheels. This may cause unexpected
behavior or breakage if the tool in question needs to specially account
for variant wheels.

The libraries for processing wheel files and their consumers will need
to be updated to handle the new filename component and possibly the new
metadata. For example, there is an `open discussion in packaging project
how to adapt the parse_wheel_filename() function
<https://github.com/pypa/packaging/issues/1148>`__.

The addition of the variant label increases the filename length. On
platforms with a low total path length limit such as Windows, long
filenames are a concern. However, given that the name and version
components are already unrestricted, we do not set a specific limit in
this PEP. Others, such as PyPI, may set a limit for total filename
length.

Aside from this explicit incompatibility, the specification makes
minimal and non-intrusive changes to the binary package format. The
`variant metadata`_ is stored in a separate file in the ``.dist-info``
directory. Tools that are not directly concerned with variants need only
to update their filename verification algorithm (if there is one) and
preserve the contents of said directory.

If the new `environment markers`_ are used in wheel dependencies, these
wheels will be incompatible with existing tools. For example, upon
meeting these markers in a dependency from an index, pip will backtrack
and use an older dependency version (if possible). This is a general
problem with the design of environment markers, and not specific to
wheel variants. It is possible to work around it by partially evaluating
environment markers at build time, and removing the markers or
dependencies specific to variant wheels from the non-variant wheel.


Security Implications
=====================

The presence of variant wheels may lead to some of the variants being
subject to less scrutiny than others, and as such becoming easier attack
targets. Particularly, once variant wheel support becomes commonplace,
the non-variant wheels for some packages may be only consumed by users
with outdated tools. However, such attacks assume that the package
publishing workflow is already compromised, in which case more plausible
attack vectors are available, for example via modifying compiled
extensions.


How to Teach This
=================

This PEP is oriented at tool authors. Its changes will be integrated
into :doc:`packaging:specifications/binary-distribution-format` and
other PyPA specifications. Teaching variants to end users will be
covered in a subsequent PEP, as user experience details are addressed.


Reference Implementation
========================

The `variantlib <https://github.com/wheelnext/variantlib>`__ project
contains a reference implementation of a complete variant wheel
solution. It is compliant with this PEP, but also goes beyond it,
providing example solutions to some of the deferred items.

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


Rejected Ideas
==============

Predictable variant labels
--------------------------

The specification proposes that variant labels are arbitrary, and
variant properties are mapped to them via a `variant metadata`_ file
rather than expressed directly in them. While it could be technically
possible to create variant labels from variant properties, this would
either require permitting very long filenames that will cause issues
with some platforms, or imposing arbitrary limits on variant property
counts, making the specification less suitable for addressing
multidimensional compatibility matrices.

An alternative approach was to use a hash of variant properties. While
such an approach is technically valid and can provide short unique
labels for arbitrarily large variant property sets, it makes the labels
opaque and therefore difficult to read or reason about.


Variant label as part of Platform compatibility tag
---------------------------------------------------

The specification adds the variant label as a separate component,
therefore breaking compatibility with existing tools. It could be
technically possible to preserve partial compatibility by appending it
to one of the Platform compatibility tags instead, in which case
installers would reject the wheel based on platform (or Python
interpreter) incompatibility, while other tools could still use it.
However, the authors decided it safer to break the backwards
compatibility. Additionally, reusing tags posed a potential risk of
wheel labels being incorrectly combined with compressed tag sets. For
example, a ``manylinux_2_27_x86_64.manylinux_2_28_x86_64+x86_64_v3`` tag
would be incorrectly deemed compatible because of the
``manylinux_2_27_x86_64`` part.


Replacing Platform compatibility tags entirely
----------------------------------------------

Technically, it would be entirely possible to convey the information
currently passed via the Platform compatibility tags via variant
properties, and remove these explicit tags from the filename. However,
we decided not to pursue this and instead preserve the existing
filenames for wheels that do not need additional variants, as we do not
believe that the effort required to update all the existing workflows
justifies the benefit of more compact, and slightly more consistent
naming.


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

The following problems are deferred to subsequent PEPs in the series:

- governance of variant namespaces
- determining which variant properties are compatible with the system
- overriding the compatibility detection using static data
- building variant wheels

In addition to that, the following matters are left
implementation-defined:

- Selecting variant wheels from multiple sources. Currently, there is no
  standard defined behavior for regular wheels, nor consensus across
  different packaging tools on how to handle that.


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
==============

- 06-Apr-2026

  - Added a formal requirement that Python tags must not start with
    a digit.
  - Expanded backwards compatibility concerns regarding tools that do
    not perform full filename verification.

- 09-Mar-2026

  - Clarified that feature values in ``variants`` dictionary are sets,
    and that they ought to be sorted when serializing.
  - Changed the rule for merging variant metadata to state that the
    result must be the same irrespective of wheel order. This conveys
    the goal of avoiding ambiguous results clearer.

- 17-Feb-2026

  - Initial version, split from :pep:`817` draft.
  - Corrected the variant ordering algorithm to order variants per the
    best value that is compatible with the system, for every feature,
    rather than all compatible values, and add a fallback to ordering
    on variant label.
  - Removed the variant label length limitation.
  - Changed ``pylock.toml`` integration to inline variant metadata
    rather than storing a URL and a hash.

- 11-May-2026

  - Added replacing platform compatibility tags entirely to rejected
    ideas.
  - Clarified interpretation of sorting algorithm and index support.


Appendices
==========

- :ref:`pep825-variant-json-schema`


Copyright
=========

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