PEP: 783
Title: Emscripten Packaging
Author: Hood Chatham <roberthoodchatham at gmail.com>
Sponsor: Łukasz Langa <lukasz at python.org>
Discussions-To: https://discuss.python.org/t/86862
Status: Accepted
Type: Standards Track
Topic: Packaging
Created: 28-Mar-2025
Post-History: `02-Apr-2025 <https://discuss.python.org/t/86862>`__,
              `18-Mar-2025 <https://discuss.python.org/t/84996>`__,
Resolution: `06-Apr-2026 <https://discuss.python.org/t/pep-783-emscripten-packaging/86862/104>`__

Abstract
========

This PEP proposes a new platform tag series ``pyemscripten`` for binary Python
package distributions for the Pyodide Python runtime.

`Emscripten <https://emscripten.org/>`__ is a complete open-source compiler
toolchain. It compiles C/C++ code into WebAssembly/JavaScript executables, for
use in JavaScript runtimes, including browsers and Node.js. The Rust language
also maintains an Emscripten target. :pep:`776` specifies Python's support for
Emscripten.


Motivation
==========

Pyodide is a CPython distribution for use in the browser. A web browser is a
universal computing platform, available on Windows, macOS, Linux, and every
smartphone. Hundreds of thousands of students have learned Python through
Pyodide via projects like `Capytale
<https://web.archive.org/web/20241211090946/https://cfp.jupytercon.com/2023/talk/TJ9YEV/>`__
and `PyodideU <https://stanford.edu/~cpiech/bio/papers/pyodideU.pdf>`__. Pyodide
is also increasingly being used by Python packages to provide interactive
documentation.

Pyodide currently maintains ports of 255 different packages at the time of this
writing, including major scientific Python packages like NumPy, SciPy, pandas,
Polars, scikit-learn, OpenCV, PyArrow, and Pillow as well as general purpose
packages like aiohttp, Requests, Pydantic, cryptography, and orjson.

About 60 packages are also testing against Pyodide in their CI, including NumPy,
pandas, awkward-cpp, scikit-image, statsmodels, PyArrow, Hypothesis, and PyO3.

Python package projects cannot deploy binary distributions for Pyodide on PyPI.
Instead they must use other options like ``anaconda.org`` or ``jsdelivr.com``.
This creates friction both for package maintainers and for users.


Rationale
=========

When Emscripten builds an application, it builds it as a free-standing program,
including the entire operating system. Emscripten primarily targets the use case
of fully static programs. When dynamic linking is used, the primary target use
case is bundle splitting and lazy loading, where the dynamic libraries are built
at the same time as the application.

As a result of that, the Emscripten compiler makes no ABI stability guarantees
between versions. Many Emscripten updates are ABI compatible by chance, and the
Rust Emscripten target behaves as if the ABI were stable with only `occasional
negative consequences <https://github.com/rust-lang/rust/issues/131467>`__.

There are several linker flags that adjust the Emscripten ABI or system
libraries. Python packages built to run with Emscripten must make sure to match
the ABI-sensitive linker flags used to compile the interpreter to avoid
load-time or run-time errors. The Emscripten compiler continuously fixes bugs
and adds support for new web platform features. Thus, there is significant
benefit to being able to update the ABI.

In order to balance the ABI stability needs of package maintainers with the ABI
flexibility to allow the platform to move forward, Pyodide plans to adopt a new
Emscripten platform for each feature release of Python which we call
``pyemscripten_${YEAR}_${PATCH}``.

The Pyodide team also coordinates the ABI flags that Pyodide uses with the
Emscripten ABI that Rust supports in order to ensure that we have support for
the many popular Rust packages. Historically, most of the work for this has
been related to unwinding ABIs. See for instance `this Rust Major Change
Proposal <https://github.com/rust-lang/compiler-team/issues/801>`__.

The ``pyemscripten`` platform has nothing specifically to do with Python and
indeed can be used by any program that uses the appropriate version of
Emscripten and the appropriate link flags. In particular, ``pyemscripten``
platform tags can be used by Python interpreters compiled and linked with the
specified version of Emscripten and with the specified ABI-sensitive flags.


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

The platform tags will take the form:

.. code-block:: text

    pyemscripten_${YEAR}_${PATCH}_wasm32

Each one of these will be used with a specified Python version. For example, the
platform tag ``pyemscripten_2026_0`` will be used with Python 3.14.

The PyEmscripten Platform
-------------------------

The specification of the ``pyemscripten_${YEAR}_${PATCH}`` platform includes:

* Which version of the Emscripten compiler is used
* What libraries are statically linked to the application
* What stack unwinding ABI is to be used
* How the loader handles dependency lookup
* That libraries cannot use ``-pthread``
* That libraries should be linked with ``-sWASM_BIGINT``

The platform is selected by choosing the appropriate version of the Emscripten
compiler and passing appropriate compiler and linker flags.

The platform definition does not include anything about Python and in particular
it is agnostic to the Python version it is intended to be used with. However,
for clarity we will note in the platform documentation which Python version we
plan to use each platform with.

The PyEmscripten platforms are fully specified in
`Pyodide's documentation on the PyEmscripten Platform <https://pyodide.org/en/stable/development/abi.html>`__.

To define the platform we need only indicate how the main application is
compiled and linked. This implies how to build a compatible shared library.
However, our documentation includes detailed instructions on how to build a
compatible shared library because we assume most people will be building shared
libraries.

The ``pyodide build`` tool knows how to create wheels that are compatible with the
PyEmscripten platform. Unlike with manylinux wheels, there is no need for a
Docker container to build ``pyemscripten`` wheels. All that is needed is a Linux
machine and appropriate versions of Python, Node.js, and Emscripten.

It is possible to validate that a wheel is compatible with the PyEmscripten
platform by installing and importing it into an appropriate version of the
Pyodide runtime. Because Pyodide can run in an environment with strong
sandboxing guarantees, doing this produces no security risks.

Determining the PyEmscripten Platform Version
---------------------------------------------

The PyEmscripten platform version is stored in the
``PYEMSCRIPTEN_PLATFORM_VERSION`` config variable and can be determined via:

.. code-block:: python

    pyemscripten_platform_version = sysconfig.get_config_var("PYEMSCRIPTEN_PLATFORM_VERSION")


To generate the list of compatible tags, one can use the following code:

.. code-block:: python

    from packaging.tags import cpython_tags, _generic_platforms

    def _emscripten_platforms() -> Iterator[str]:
        pyemscripten_platform_version = sysconfig.get_config_var("PYEMSCRIPTEN_PLATFORM_VERSION")
        if pyemscripten_platform_version:
            yield f"pyemscripten_{pyemscripten_platform_version}_wasm32"
        yield from _generic_platforms()

    emscripten_tags = cpython_tags(platforms=_emscripten_platforms())

This code will be added to `pypa/packaging
<https://github.com/pypa/packaging/pull/804>`__.


Package Installers
------------------

Installers should use the ``_emscripten_platforms()`` function shown above to
determine which platforms are compatible with an Emscripten build of CPython. In
particular, the PyEmscripten platform version is exposed via
``sysconfig.get_config_var("PYEMSCRIPTEN_PLATFORM_VERSION")``.

Package Indexes
---------------

Package indexes SHOULD accept any wheel whose platform tag matches
the regular expression ``pyemscripten_[0-9]+_[0-9]+_wasm32``.


Dependency Specifier Markers
----------------------------

According to :pep:`776#platform-identification`, in Emscripten Python
``sys.platform`` returns ``"emscripten"``. To check for the Emscripten platform in a
dependency specifier, one can use ``sys_platform == "emscripten"`` (or its
negation).


Trove Classifier
----------------

Packages that build and test Emscripten wheels can declare this by adding the
``Environment :: WebAssembly :: Emscripten`` classifier. PyPI already accepts uploads of
`packages with this classifier
<https://pypi.org/search/?q=&o=&c=Environment+%3A%3A+WebAssembly+%3A%3A+Emscripten>`__.


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

There are no backwards compatibility concerns in this PEP.


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

There are no security implications in this PEP.

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

A Custom Interpreter Tag For Pyodide
------------------------------------

We don't need a custom interpreter tag for Pyodide because Pyodide is CPython.
While we do apply a few minor patches, they have no effect on the interpreter ABI
and our long term goal is to upstream everything.

Alternative Options for the Platform Tag
----------------------------------------

``emscripten_${EMSCRIPTEN_VERSION}``
    It is tempting to use ``emscripten`` as the platform tag because the
    ``pyemscripten`` platform has nothing specifically to do with Python and
    indeed can be used by any program that uses the appropriate version of
    Emscripten and the appropriate link flags. But
    ``emscripten_${EMSCRIPTEN_VERSION}`` is too vague by itself because the platform
    also depends on various linker flags.

    There are other communities which have similar problems and would also
    benefit from a centralized standard for "Long Term Service" Emscripten
    platforms that the whole ecosystem could use. However, the Emscripten team
    have so far not been willing to provide a this standard since they consider
    dynamic linking an unusual use case. Thus it is left for our ecosystem to
    solve the problem itself. The platform tag should contain some indication
    of this.

``pyemscripten_${PYTHON_MAJOR_MINOR}_${PATCH}``
    This would make it clearer which Python version is meant for use with each
    platform, but it leads to conceptual confusion since the platform has
    nothing to do with Python.

``pyodide_...``
    For now the platform is defined by Pyodide so this connection would be made
    clearer by calling the platform ``pyodide``. But the capabilities of the
    platform are tied to what Emscripten supports not on what Pyodide supports so
    the platform tag should be focused on Emscripten. The ``pyemscripten`` tag is
    also more forwards compatible to a future where the definition of the platform
    moves upstream of Pyodide.

No patch version
    We hope never to need the patch version, but it's good to be prepared for
    unforseen problems.

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

For Pyodide Users
-----------------
We recommend the `Pyodide documentation on installing packages
<https://pyodide.org/en/stable/usage/loading-packages.html>`__. We will make a
table showing which ``pyemscripten`` platform version each Pyodide version is
compatible with.

For Package Maintainers
-----------------------

We recommend the `Pyodide documentation on building and testing packages
<https://pyodide.org/en/stable/development/building-packages-from-source.html>`__.
The Scientific Python community is also working on
`a spec <https://github.com/scientific-python/specs/pull/388/>`_
which describes to package maintainers how to maintain web-based interactive
documentation using Emscripten-based Python.

Generally cibuildwheel is the easiest way to build and test a package for use
with Pyodide. Maintainers can also use ``pyodide-build`` directly to build a
package. Rust packages that use Maturin as their build system can be built
directly with Maturin since it has native support for cross builds.

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

For building packages, `pyodide build
<https://github.com/pyodide/pyodide-build>`__ and `cibuildwheel
<https://github.com/pypa/cibuildwheel/>`__.

For installers to decide whether a wheel tag is compatible with a Pyodide
interpreter, `pypa/packaging#804
<https://github.com/pypa/packaging/pull/804>`__.

Copyright
=========

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