PEP 725 – Specifying external dependencies in pyproject.toml
- Author:
- Pradyun Gedam <pradyunsg at gmail.com>, Jaime Rodríguez-Guerra <jaime.rogue at gmail.com>, Ralf Gommers <ralf.gommers at gmail.com>
- Discussions-To:
- Discourse thread
- Status:
- Draft
- Type:
- Standards Track
- Topic:
- Packaging
- Created:
- 17-Aug-2023
- Post-History:
- 18-Aug-2023, 22-Sep-2025
Table of Contents
- Abstract
- Motivation
- Rationale
- Specification
- Backwards Compatibility
- Security Implications
- How to Teach This
- Reference Implementation
- Rejected Ideas
- Specific syntax for external dependencies which are also packaged on PyPI
- Using library and header names as external dependencies
- Splitting host dependencies with explicit
-dev
or-devel
suffixes - Identifier indirections
- Adding a
host-requires
key under[build-system]
- Reusing the
Requires-External
field in Core Metadata - Allowing use of ecosystem-specific version comparison semantics
- Open Issues
- References
- Copyright
Abstract
This PEP specifies how to write a project’s external, or non-PyPI, build and
runtime dependencies in a pyproject.toml
file for packaging-related tools
to consume.
This PEP proposes to add an [external]
table to pyproject.toml
with
seven keys. “build-requires”, “host-requires” and “dependencies”
are for specifying three types of required dependencies:
build-requires
, build tools to run on the build machinehost-requires
, build dependencies needed for the host machine but also needed at build time.dependencies
, needed at runtime on the host machine but not needed at build time.
These three keys also have their optional external
counterparts (optional-build-requires
,
optional-host-requires
, optional-dependencies
), which have the same role that
project.optional-dependencies
plays for project.dependencies
. Finally,
dependency-groups
offers the same functionality as PEP 735 but for external
dependencies.
Cross compilation is taken into account by distinguishing between build and host dependencies.
Optional build-time and runtime dependencies are supported too, in a manner analogous
to how that is supported in the [project]
table.
Motivation
Python packages may have dependencies on build tools, libraries, command-line tools, or other software that is not present on PyPI. Currently there is no way to express those dependencies in standardized metadata [1], [2]. Key motivators for this PEP are to:
- Enable tools to automatically map external dependencies to packages in other packaging repositories,
- Make it possible to include needed dependencies in error messages emitted by Python package installers and build frontends,
- Provide a canonical place for package authors to record this dependency information.
Packaging ecosystems like Linux distros, conda, Homebrew, Spack, and Nix need
full sets of dependencies for Python packages, and have tools like pyp2spec
(Fedora), Grayskull (conda), and dh_python (Debian) which attempt to
automatically generate dependency metadata for their own package managers from the metadata in
upstream Python packages. External dependencies are currently handled manually,
because there is no metadata for this in pyproject.toml
or any other
standard location. Other tools resort to extracting dependencies from extension
modules and shared libraries inside Python packages, like elfdeps (Fedora).
Enabling automating this type of conversion by only using explicitly annotated metadata
is a key benefit of this PEP, making packaging Python packages for distros easier
and more reliable. In addition, the authors envision other types of tools
making use of this information, e.g., dependency analysis tools like Repology,
Dependabot and libraries.io.
Software bill of materials (SBOM) generation tools may also be able to use this
information, e.g. for flagging that external dependencies listed in
pyproject.toml
but not contained in wheel metadata are likely vendored
within the wheel. PEP 770, which standardizes how SBOMs are included in
wheels, contains an instructive section on how that PEP differs from this one.
Packages with external dependencies are typically hard to build from source, and error messages from build failures tend to be hard to decipher for end users. Missing external dependencies on the end user’s system are the most likely cause of build failures. If installers can show the required external dependencies as part of their error message, this may save users a lot of time.
At the moment, information on external dependencies is only captured in installation documentation of individual packages. It is hard to maintain for package authors and tends to go out of date. It’s also hard for users and distro packagers to find it. Having a canonical place to record this dependency information will improve this situation.
This PEP is not trying to specify how the external dependencies should be used, nor a mechanism to implement a name mapping from names of individual packages that are canonical for Python projects published on PyPI to those of other packaging ecosystems. Canonical names and a name mapping mechanism are addressed in PEP 804.
Rationale
Types of external dependencies
Multiple types of external dependencies can be distinguished:
- Concrete packages that can be identified by name and have a canonical location in another language-specific package repository. E.g., Rust packages on crates.io, R packages on CRAN, JavaScript packages on the npm registry.
- Concrete packages that can be identified by name but do not have a clear canonical location. This is typically the case for libraries and tools written in C, C++, Fortran, CUDA and other low-level languages. E.g., Boost, OpenSSL, Protobuf, Intel MKL, GCC.
- “Virtual” packages, which are names for concepts, types of tools or interfaces. These typically have multiple implementations, which are concrete packages. E.g., a C++ compiler, BLAS, LAPACK, OpenMP, MPI.
Concrete packages are straightforward to understand, and are a concept present in every package management system. Virtual packages are a concept also present in a number of packaging systems – but not always, and the details of their implementation vary.
Cross compilation
Cross compilation is not yet (as of September 2025) well-supported by stdlib
modules and pyproject.toml
metadata. It is however important when
translating external dependencies to those of other packaging systems (with
tools like pyp2spec
). Introducing support for cross compilation immediately
in this PEP is much easier than extending [external]
in the future, hence
the authors choose to include this now.
Terminology
This PEP uses the following terminology:
- build machine: the machine on which the package build process is being executed.
- host machine: the machine on which the produced artifact will be installed and run.
- build dependency: package required only during the build process. It must be available at build time and is built for the build machine’s OS and architecture. Typical examples include compilers, code generators, and build tools.
- host dependency: package needed during the build and often also at runtime. It must be available during the build and is built for the host machine’s OS and architecture. These are usually libraries the project links against.
- runtime dependency: package required only when the package is used after installation. It is not required at build time but must be available on the host machine at runtime.
Note that this terminology is not consistent across build and packaging tools,
so care must be taken when comparing build/host dependencies in
pyproject.toml
to dependencies from other package managers.
Note that “target machine” or “target dependency” are not used in this PEP. That is typically only relevant for cross-compiling a compiler or other such advanced scenarios [3], [4] – this is out of scope for this PEP.
Finally, note that while “dependency” is the term most widely used for packages
needed at build time, the existing key in pyproject.toml
for PyPI
build-time dependencies is build-requires
. Hence this PEP uses the keys
build-requires
and host-requires
under [external]
for consistency.
Build and host dependencies
Clear separation of metadata associated with the definition of build and host platforms, rather than assuming that build and host platform will always be the same, is important [5].
Build dependencies are typically run during the build process - they may be
compilers, code generators, or other such tools. In case the use of a build
dependency implies a runtime dependency, that runtime dependency does not have
to be declared explicitly. For example, when compiling Fortran code with
gfortran
into a Python extension module, the package likely incurs a
dependency on the libgfortran
runtime library. The rationale for not
explicitly listing such runtime dependencies is two-fold: (1) it may depend on
compiler/linker flags or details of the build environment whether the
dependency is present, and (2) these runtime dependencies can be detected and
handled automatically by tools like auditwheel
.
Host dependencies are typically not run during the build process, but only used for linking against. This is not a rule though – it may be possible or necessary to run a host dependency under an emulator, or through a custom tool like crossenv. When host dependencies imply a runtime dependency, that runtime dependency also does not have to be declared, just like for build dependencies.
When host dependencies are declared and a tool which is executing an action
unrelated to cross-compiling, it may decide to merge the host-requires
list
into build-requires
- whether this is useful is context-dependent.
Specifying external dependencies
Concrete package specification
A “Package URL” or PURL is a widely used URL string for identifying packages that is meant to be portable across packaging ecosystems. Its design is:
scheme:type/namespace/name@version?qualifiers#subpath
The scheme
component is a fixed string, pkg
, and of the other
components only type
and name
are required.
Since external dependencies are likely to be typed by hand, we propose a PURL derivative that, in the name of ergonomics and user-friendliness, introduces a number of changes (further discussed below):
- Support for virtual packages via a new
virtual
type. - Allow version ranges (and not just literals) in the
version
field.
In this derivative, we replace the pkg
scheme with dep
. Hence,
we will refer to them as DepURLs.
As an example, a DepURL for the requests
package on PyPI would be:
dep:pypi/requests
# equivalent to pkg:pypi/requests
Adopting PURL-compatible strings to specify external dependencies in
pyproject.toml
solves a number of problems at once, and there are already
implementations of the specification in Python and multiple other languages. PURL is
also already supported by dependency-related tooling like SPDX (see
External Repository Identifiers in the SPDX 2.3 spec),
the Open Source Vulnerability format,
and the Sonatype OSS Index;
not having to wait years before support in such tooling arrives is valuable.
DepURLs are very easily transformed into PURLs, with the exception of
dep:virtual
which doesn’t have an equivalent in PURL.
For concrete packages without a canonical package manager to refer to, either
dep:generic/dep-name
can be used, or a direct reference to the VCS system
that the package is maintained in (e.g.,
dep:github/user-or-org-name/dep-name
). Which of these is more appropriate
is situation-dependent. This PEP recommends using dep:generic
when the
package name is unambiguous and well-known (e.g., dep:generic/git
or
dep:generic/openblas
), and using the VCS as the type otherwise. Which name
is chosen as canonical for any given package, as well as the process to make
and record such choices, is the topic of PEP 804.
Virtual package specification
PURL does not offer support for virtual or virtual dependency specification yet. A proposal to add a virtual type is being discussed for revision 1.1.
In the meantime, we propose adding a new type to our dep:
derivative, the virtual
type, which can take two namespaces (extensible through the process given in
PEP 804):
interface
: for components such as BLAS or MPI.compiler
: for compiled languages like C or Rust.
The name should be the most common name for the interface or language, lowercased. Some examples include:
dep:virtual/compiler/c
dep:virtual/compiler/cxx
dep:virtual/compiler/rust
dep:virtual/interface/blas
dep:virtual/interface/lapack
Since there are a limited number of such dependencies, it seems like it will be understandable and map well to Linux distros with virtual packages and to the likes of conda and Spack.
Versioning
PURLs support fixed versions via the @
component of the URL. For example,
numpy===2.0
can be expressed as pkg:pypi/numpy@2.0
.
Support in PURL for version expressions and ranges beyond a fixed version is
available via vers
URIs (see specification):
vers:type/version-constraint|version-constraint|...
Users are supposed to couple a pkg:
URL with a vers:
URL. For example,
to express numpy>=2.0
, the PURL equivalent would be pkg:pypi/numpy
plus
vers:pypi/>=2.0
. This can be done with:
- A two-item list:
["pkg:pypi/numpy", "vers:pypi/>=2.0"]
. - A percent-encoded
URL qualifier:
pkg:pypi/numpy?vers=vers:pypi%2F%3E%3D2.0
.
Since none of these options are very ergonomic, we chose instead for DepURLs
to accept version range specifiers too with semantics that are a subset of
PEP 440 semantics. The allowed operators are those that are widely available
across package managers (e.g., ==
, >
and >=
are common, while
~=
isn’t).
Some examples:
dep:pypi/numpy@2.0
:numpy
pinned at exactly version 2.0.dep:pypi/numpy@>=2.0
:numpy
with version greater or equal than 2.0.dep:virtual/interface/lapack@>=3.7.1
: any package implementing the LAPACK interface for version greater or equal than3.7.1
.
The versioning scheme for particular virtual packages, in case that isn’t unambiguously defined by an upstream project or standard, will be defined in the Central Registry (see PEP 804).
Environment markers
Regular environment markers (as originally defined in PEP 508) may
be used behind DepURLs. PURL qualifiers, which use ?
followed by a package
type-specific dependency specifier component, should not be used for the
purposes for which environment markers suffice. The reason for this is
pragmatic: environment markers are already used for other metadata in
pyproject.toml
, hence any tooling that is used with pyproject.toml
is
likely to already have a robust implementation to parse it. And we do not
expect to need the extra possibilities that PURL qualifiers provide (e.g., to
specify a Conan or conda channel, or a RubyGems platform).
We name the combination of a DepURL and environment markers as “external dependency specifiers”, analogously to the existing dependency specifiers.
Canonical names of dependencies and -dev(el)
split packages
It is fairly common, but far from universal, for distros to split a package
into two or more packages. In particular, runtime components are often
separately installable from development components (headers, pkg-config and
CMake files, etc.). The latter then typically has a name with -dev
or
-devel
appended to the project/library name. Also, larger packages are
sometimes split into multiple separate packages to keep install sizes
manageable. More often than not, such package splits are not defined or
recognized by the maintainers of a package, and it’s therefore ambiguous what
any split would mean. Hence, such splits should not be reflected in the
[external]
table. It is not possible to specify this in a reasonable way
that works across distros, hence only the canonical name should be used in
[external]
.
The intended meaning of using a DepURL is “the full package with the name
specified”. I.e., including all installable artifacts that are part of the
package. It will depend on the context in which the metadata is used whether a
package split is relevant. For example, if libffi
is a host
dependency and a tool wants to prepare an environment for building a wheel,
then if a distro has split off the headers for libffi
into a
libffi-devel
package then the tool has to install both libffi
and
libffi-devel
.
For defining what canonical package names are and how package splits are
handled in practice when tools attempt to use [external]
for installation
purposes, we refer to PEP 804.
Python development headers
Python headers and other build support files may also be split. This is the
same situation as in the section above (because Python is simply a regular
package in distros). However, a python-dev|devel
dependency is special because
in pyproject.toml
Python itself is an implicit rather than an explicit
dependency. Hence a choice needs to be made here - add python-dev
implicitly,
or make each package author add it explicitly under [external]
. For
consistency between Python dependencies and external dependencies, we choose to
add it implicitly. Python development headers must be assumed to be necessary
when an [external]
table contains one or more compiler packages.
New Core Metadata fields
Two new Core Metadata fields are proposed:
Requires-External-Dep
. An external requirement. Mimics the transition fromRequires
toRequires-Dist
. We chose the-Dep
suffix to emphasize that the value is not a regular Python specifier (distribution), but an external dependency specifier containing a DepURL.Provides-External-Extra
. An extra group that carries external dependencies (as found inRequires-External-Dep
) only.
Since the Core Metadata specification does not contain fields for any metadata in
pyproject.toml
’s [build-system]
table, the build-requires
and host-requires
content do not need to be reflected in existing core
metadata fields.
Additionally, this PEP also proposes to deprecate the Requires-External
field.
The reasons being:
- Avoiding confusion with the newly proposed fields.
- Avoiding potential incompatibilities with existing usage (even if limited).
- Low penetration in the ecosystem:
- There is no direct correspondence to a field in the
pyproject.toml
metadata. - Mainstream build backends like
setuptools
(see pypa/setuptools#4220),hatch
(see pypa/hatch#1712),flit
(see pypa/flit#353), orpoetry
do not offer ways to specify it or require a plugin (e.g. poetry-external-dependencies).maturin
does seem to support it since 0.7.0 (see PyO3/maturin@5b0e4808), but it’s not directly documented. Other backends likescikit-build-core
ormeson-python
returned no results forExternal-Requires
. - The field is not included in the PyPI JSON API responses.
- There is no direct correspondence to a field in the
Dependency groups
This PEP has chosen to include the PEP 735 key dependency-groups
under
the [external]
table too. This decision is motivated by the need of having
similar functionality for external metadata. The top-level table cannot be used
for external dependencies because it’s expected to have PEP 508 strings (and tables
for group includes), while we have chosen to rely on dep:
URLs for the external
dependencies. Conflating both would raise significant backwards compatibility
issues with existing usage.
Strictly speaking, the dependency-groups
schema allows us to define external
dependencies in per-group sub-tables:
[dependency-groups]
dev = [
"pytest",
{ external = ["dep:cargo/ripgrep"] },
]
However, this has the same problem: we are mixing different types of dependency
specifiers in the same data structure. We believe it’s cleaner to separate concerns
in different top-level tables, hence why we still prefer to have
external.dependency-groups
.
Optional dependencies versus dependency groups
The rationale for having external.dependency-groups
is identical for the
rationale given in PEP 735 for introducing [dependency-groups]
. The
intended usage and semantics of inclusion/exclusion into Core Metadata
is thus identical to [dependency-groups]
.
external.optional-dependencies
will show up in Core Metadata.
external.dependency-groups
will not.
Specification
If metadata is improperly specified then tools MUST raise an error to notify the user about their mistake.
DepURL
A DepURL implements a scheme for identifying packages that is meant to be portable across packaging ecosystems. Its design is:
dep:type/namespace/name@version?qualifiers#subpath
dep:
is a fixed string, and always present. type
and name
are
required, other components are optional. All components apply for both PURL
and virtual type
’s, and have these requirements:
type
(required): MUST be either a PURLtype
, orvirtual
.namespace
(optional): MUST be a PURLnamespace
, or a namespace in the DepURL central registry (see PEP 804).name
(required): MUST be a name that parses as a valid PURLname
. Tools MAY warn or error if a name is not present in the DepURL central registry (see PEP 804).version
(optional): MUST be a regular version specifier (PEP 440 semantics) as a single version or version range, with the restriction that only the following operators may be used:>=
,>
,<
,<=
,==
,,
.qualifiers
(optional): MUST parse as a valid PURLqualifier
.subpath
(optional): MUST parse as a valid PURLsubpath
.
External dependency specifiers
External dependency specifiers MUST contain a DepURL, and MAY contain environment markers with the same syntax as used in regular dependency specifiers (as originally specified in PEP 508).
Changes in Core Metadata
Deprecations
The External-Requires
Core Metadata field will be marked as obsolete and its
usage will be discouraged.
Additions
Two new fields are added to Core Metadata:
Requires-External-Dep
. An external requirement expressed as an external dependency specifier string.Provides-External-Extra
. An extra group that carries external dependencies (as found inRequires-External-Dep
) only.
Version bump
Given that the proposed changes are purely additive, the Core Metadata version will be bumped to 2.6.
This will only impact PyPI and tools that want to support external runtime dependencies, and require no changes otherwise.
Changes in pyproject.toml
Note that pyproject.toml
content is in the same format as in PEP 621.
Table name
Tools MUST specify fields defined by this PEP in a table named [external]
.
No tools may add fields to this table which are not defined by this PEP or
subsequent PEPs. The lack of an [external]
table means the package either
does not have any external dependencies, or the ones it does have are assumed
to be present on the system already.
build-requires
/optional-build-requires
- Format: Array of external dependency specifiers (
build-requires
) and a table with values of arrays of external dependency specifiers (optional-build-requires
) - Core metadata: N/A
The (optional) external build requirements needed to build the project.
For build-requires
, it is a key whose value is an array of strings. Each
string represents a build requirement of the project and MUST be formatted as
a valid external dependency specifier.
For optional-build-requires
, it is a table where each key specifies an
extra set of build requirements and whose value is an array of strings. The
strings of the arrays MUST be valid external dependency specifiers.
host-requires
/optional-host-requires
- Format: Array of external dependency specifiers (
host-requires
) and a table with values of arrays of external dependency specifiers (optional-host-requires
) - Core metadata: N/A
The (optional) external host requirements needed to build the project.
For host-requires
, it is a key whose value is an array of strings. Each
string represents a host requirement of the project and MUST be formatted as
a valid external dependency specifier.
For optional-host-requires
, it is a table where each key specifies an
extra set of host requirements and whose value is an array of strings. The
strings of the arrays MUST be valid external dependency specifiers.
dependencies
/optional-dependencies
- Format: Array of external dependency specifiers (
dependencies
) and a table with values of arrays of external dependency specifiers (optional-dependencies
) - Core metadata:
Requires-External-Dep
,Provides-External-Extra
The (optional) runtime dependencies of the project.
For dependencies
, it is a key whose value is an array of strings. Each
string represents a dependency of the project and MUST be formatted as a valid
external dependency specifier. Each string must be added to Core Metadata as
a Requires-External-Dep
field.
For optional-dependencies
, it is a table where each key specifies an extra
and whose value is an array of strings. The strings of the arrays MUST be valid
external dependency specifiers. For each optional-dependencies
group:
- The name of the group MUST be added to Core Metadata as a
Provides-External-Extra
field. - The external dependency specifiers in that group MUST be added to Core
Metadata as a
Requires-External-Dep
field, with the corresponding; extra == 'name'
environment marker.
dependency-groups
- Format: A table where each key is the name of the group, and the values are arrays of external dependency specifiers, tables, or a mix of both.
- Core metadata: N/A
PEP 735 -style dependency groups, but using external dependency specifiers instead of PEP 508 strings. Every other detail (e.g. group inclusion, name normalization) follows the official dependency groups specification.
Examples
These examples show what the [external]
table content for a number of
packages, and the corresponding PKG-INFO
/METADATA
content (if any) is
expected to be.
cryptography 39.0
pyproject.toml
content:
[external]
build-requires = [
"dep:virtual/compiler/c",
"dep:virtual/compiler/rust",
"dep:generic/pkg-config",
]
host-requires = [
"dep:generic/openssl",
"dep:generic/libffi",
]
PKG-INFO
/ METADATA
content: N/A.
SciPy 1.10
pyproject.toml
content:
[external]
build-requires = [
"dep:virtual/compiler/c",
"dep:virtual/compiler/cpp",
"dep:virtual/compiler/fortran",
"dep:generic/ninja",
"dep:generic/pkg-config",
]
host-requires = [
"dep:virtual/interface/blas",
"dep:virtual/interface/lapack@>=3.7.1",
]
PKG-INFO
/ METADATA
content: N/A.
Pillow 10.1.0
pyproject.toml
content:
[external]
build-requires = [
"dep:virtual/compiler/c",
]
host-requires = [
"dep:generic/libjpeg",
"dep:generic/zlib",
]
[external.optional-host-requires]
extra = [
"dep:generic/lcms2",
"dep:generic/freetype",
"dep:generic/libimagequant",
"dep:generic/libraqm",
"dep:generic/libtiff",
"dep:generic/libxcb",
"dep:generic/libwebp",
"dep:generic/openjpeg@>=2.0",
"dep:generic/tk",
]
PKG-INFO
/ METADATA
content: N/A.
Spyder 6.0
pyproject.toml
content:
[external]
dependencies = [
"dep:cargo/ripgrep",
"dep:cargo/tree-sitter-cli",
"dep:golang/github.com/junegunn/fzf",
]
PKG-INFO
/ METADATA
content:
Requires-External-Dep: dep:cargo/ripgrep
Requires-External-Dep: dep:cargo/tree-sitter-cli
Requires-External-Dep: dep:golang/github.com/junegunn/fzf
jupyterlab-git 0.41.0
pyproject.toml
content:
[external]
dependencies = [
"dep:generic/git",
]
[external.optional-build-requires]
dev = [
"dep:generic/nodejs",
]
PKG-INFO
/ METADATA
content:
Requires-External-Dep: dep:generic/git
PyEnchant 3.2.2
pyproject.toml
content:
[external]
dependencies = [
# libenchant is needed on all platforms but vendored into wheels
# distributed on PyPI for Windows. Hence choose to encode that in
# the metadata. Note: there is no completely unambiguous way to do
# this; another choice is to leave out the environment marker in the
# source distribution and either live with the unnecessary ``METADATA``
# entry in the distributed Windows wheels, or to apply a patch to this
# metadata when building those wheels.
"dep:github/AbiWord/enchant; platform_system!='Windows'",
]
PKG-INFO
/ METADATA
content:
Requires-External-Dep: dep:github/AbiWord/enchant; platform_system!="Windows"
With dependency groups
pyproject.toml
content:
[external.dependency-groups]
dev = [
"dep:generic/catch2",
"dep:generic/valgrind",
]
PKG-INFO
/ METADATA
content: N/A.
Backwards Compatibility
There is no impact on backwards compatibility, as this PEP only adds new, optional metadata. In the absence of such metadata, nothing changes for package authors or packaging tooling.
The only change introduced in this PEP that has impact on existing projects is the
deprecation of the External-Requires
Core Metadata field. We estimate the impact
of this deprecation to be negligible, given the its low penetration in the ecosystem
(see Rationale).
The field will still be recognized by existing tools such as setuptools-ext
but its usage will be discouraged in the Python Packaging User Guide, similar to
what is done for obsolete fields like Requires
(deprecated in favor of
Requires-Dist
).
Security Implications
There are no direct security concerns as this PEP covers how to statically define metadata for external dependencies. Any security issues would stem from how tools consume the metadata and choose to act upon it.
How to Teach This
External dependencies and if and how those external dependencies are vendored
are topics that are typically not understood in detail by Python package
authors. We intend to start from how an external dependency is defined, the
different ways it can be depended on—from runtime-only with ctypes
or a
subprocess
call to it being a build dependency that’s linked against—
before going into how to declare external dependencies in metadata. The
documentation should make explicit what is relevant for package authors, and
what for distro packagers.
Material on this topic will be added to the most relevant packaging tutorials,
primarily the Python Packaging User Guide. In addition, we expect that any
build backend that adds support for external dependencies metadata will include
information about that in its documentation, as will tools like auditwheel
.
Reference Implementation
This PEP contains a metadata specification, rather that a code feature - hence there will not be code implementing the metadata spec as a whole. However, there are parts that do have a reference implementation:
- The
[external]
table has to be valid TOML and therefore can be loaded withtomllib
. This table can be further processed with the pyproject-external package, demonstrated below. - The PURL specification, as a key part of this spec, has a Python package with a reference implementation for constructing and parsing PURLs: packageurl-python. This package is wrapped in pyproject-external to provide DepURL-specific validation and handling.
There are multiple possible consumers and use cases of this metadata, once that metadata gets added to Python packages. Tested metadata for all of the top 150 most-downloaded packages from PyPI with published platform-specific wheels can be found in rgommers/external-deps-build. This metadata has been validated by using it to build wheels from sdists patched with that metadata in clean Docker containers.
Example
Given a pyproject.toml
with this [external]
table:
[external]
build-requires = [
"dep:virtual/compiler/c",
"dep:virtual/compiler/rust",
"dep:generic/pkg-config",
]
host-requires = [
"dep:generic/openssl",
"dep:generic/libffi",
]
You can use pyproject_external.External
to parse it and manipulate it:
>>> from pyproject_external import External
>>> external = External.from_pyproject_path("./pyproject.toml")
>>> external.validate()
>>> external.to_dict()
{'external': {'build_requires': ['dep:virtual/compiler/c', 'dep:virtual/compiler/rust', 'dep:generic/pkg-config'], 'host_requires': ['dep:generic/openssl', 'dep:generic/libffi']}}
>>> external.build_requires
[DepURL(type='virtual', namespace='compiler', name='c', version=None, qualifiers={}, subpath=None), DepURL(type='virtual', namespace='compiler', name='rust', version=None, qualifiers={}, subpath=None), DepURL(type='generic', namespace=None, name='pkg-config', version=None, qualifiers={}, subpath=None)]
>>> external.build_requires[0]
DepURL(type='virtual', namespace='compiler', name='c', version=None, qualifiers={}, subpath=None)
Note the proposed [external]
table was well-formed. With invalid contents such as:
[external]
build-requires = [
"dep:this-is-missing-the-type",
"pkg:not-a-dep-url"
]
You would fail the validation:
>>> external = External.from_pyproject_data(
{
"external": {
"build_requires": [
"dep:this-is-missing-the-type",
"pkg:not-a-dep-url"
]
}
}
)
ValueError: purl is missing the required type component: 'dep:this-is-missing-the-type'.
Rejected Ideas
Specific syntax for external dependencies which are also packaged on PyPI
There are non-Python packages which are packaged on PyPI, such as Ninja, patchelf and CMake. What is typically desired is to use the system version of those, and if it’s not present on the system then install the PyPI package for it. The authors believe that specific support for this scenario is not necessary (or at least, too complex to justify such support); a dependency provider for external dependencies can treat PyPI as one possible source for obtaining the package. An example mapping for this use case is proposed in PEP 804.
Using library and header names as external dependencies
A previous draft PEP (“External dependencies” (2015)) proposed using specific library and header names as external dependencies. This is both too granular, and insufficient (e.g., headers are often unversioned; multiple packages may provide the same header or library). Using package names is a well-established pattern across packaging ecosystems and should be preferred.
Splitting host dependencies with explicit -dev
or -devel
suffixes
This convention is not consistent across packaging ecosystems, nor commonly
accepted by upstream package authors. Since the need for explicit control
(e.g., installing headers when a package is used as a runtime rather than a
build-time dependency) is quite niche and we don’t want to add design
complexity without enough clear use cases, we have chosen to rely solely on the
build
, host
and run
category split, with tools being in charge of
which category applies to each case in a context-dependent way.
If this proves to be insufficient, a future PEP could use the URL qualifier
features present in the PURL schema (?key=value
) to implement the necessary
adjustments. This can be done in a backwards compatible fashion.
Identifier indirections
Some ecosystems exhibit methods to select packages based on parametrized
functions like cmake("dependency")
or compiler("language")
, which
return package names based on some additional context or configuration. This
feature is arguably not very common and, even when present, rarely used.
Additionally, its dynamic nature makes it prone to changing meaning over time,
and relying on specific build systems for the name resolution is in general not
a good idea.
The authors prefer static identifiers that can be mapped explicitly via well known metadata (e.g., as proposed in PEP 804).
Ecosystems that do implement these indirections can use them to support the infrastructure designed to generate the mappings proposed in PEP 804.
Adding a host-requires
key under [build-system]
Adding host-requires
for host dependencies that are on PyPI in order to
better support name mapping to other packaging systems with support for
cross-compiling seems useful in principle, for the same reasons as this PEP
adds a host-requires
under the [external]
table. However, it isn’t
necessary to include in this PEP, and hence the authors prefer to keep the
scope of this PEP limited - a future PEP on cross compilation may want to
tackle this. This issue
contains more arguments in favor and against adding host-requires
under
[build-system]
as part of this PEP.
Reusing the Requires-External
field in Core Metadata
The Core Metadata specification contains one relevant field, namely
Requires-External
. While at first sight it would be a good candidate to
record the external.dependencies
table, the authors have decided to not
re-use this field to propagate the external runtime dependencies metadata.
The Requires-External
field has very loosely defined semantics as of
version 2.4. Essentially: name [(version)][; environment marker]
(with
square brackets denoting optional fields). It is not defined what valid strings
for name
are; the example in the specification uses both “C” as a language
name, and “libpng” as a package name. Tightening up the semantics would be
backwards incompatible, and leaving it as is seems unsatisfactory. DepURLs
would need to be decomposed to fit in this syntax.
Allowing use of ecosystem-specific version comparison semantics
There are cases, in particular when dealing with pre-releases, where PEP 440
semantics for version comparisons don’t quite work. For example, 1.2.3a
may
indicate a release subsequent to 1.2.3
rather than an alpha version. To
handle such cases correctly, it would be necessary to allow arbitrary
versioning schemes. The authors of this PEP consider the added value of
allowing that is not justified by the additional complexity. If desired, a
package author can use either a code comment or the qualifier
field of a
DepURL (see the Versioning section under Rationale) to capture this level of
detail.
Open Issues
None at this time.
References
Copyright
This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.
Source: https://github.com/python/peps/blob/main/peps/pep-0725.rst
Last modified: 2025-09-29 13:22:31 GMT