Limitations¶
What does not work in the guest, or works differently from native Python, and why. Most of it follows from the platform: wasm32-wasip1 is a 32-bit machine with WASI's system interface, which has no threads, no processes, no network and no shared libraries. The project documents these facts rather than patching them away. Each fact below has its reason beside it.
A known failure in a package's own test suite, or in one of the project's checks, points at a section of this page.
The platform's facts¶
python.wasm -V prints them after its version:
(wasm32-wasip1: numpy's default integer is 32-bit; no threads; no ctypes, mmap, sockets, ssl, zstd or scipy.odr; compute about 1.3 to 2.3 times slower than native)
- numpy's default integer is 32-bit.
wasm32's Clongis 32 bits, and numpy's default integer follows it. Sonp.arange(1_000_000).sum()wraps; passdtype=np.int64. - No threads.
wasm32-wasip1has no threads. Code that starts one fails, and nothing runs in parallel inside the guest. - No processes. WASI has no
forkorexec, sosubprocessandmultiprocessingcannot start anything. - No network. The guest has no sockets. Data comes in through a grant, a mount, standard input or a host tool.
- No
_ctypes. WASI has nodlopen, so no library can be loaded.import ctypesworks: it sizes wasm32's C types and gives numpy their dtypes, but loading a library or calling through a pointer raisesOSError. - No
mmap. WASI cannot map a file into memory. A memory map (joblib'smmap_mode, for one) raisesOSError, and Pillow falls back to reading the file, silently. - No
_ssland no_zstd. There is no network for TLS to protect, and libzstd is not built. - No
scipy.odr. It needs ODRPACK, which is Fortran, and scipy has deprecated it for removal.import scipy.odrraisesImportErrorsaying so. - No
dup. WASI cannot duplicate a descriptor.os.dup,os.dup2andos.pipe2exist and raiseOSError(ENOTSUP);os.pipeis absent.ndarray.tofileandnp.fromfileon real files fail for the same reason; usetobytes()andnp.frombuffer, orio.BytesIO.np.saveandnp.loadof paths and open files work. - No
resourcemodule. WASI has nogetrusageorsetrlimit, soimport resourceraisesModuleNotFoundError. - No floating-point status flags. WebAssembly arithmetic sets none, so what only the hardware signals stays silent:
np.array([1.0]) / 0,np.log(0.0)andnp.sqrt(-1.0)give native's values with noRuntimeWarning, andnp.errstate(all="raise")does not raise for them. Check withnumpy.isfinitewhere it matters. numpy's own software signals (integer division by zero, float16 overflow) warn and raise as natively. - No
/dev/nulland no permission bits.os.devnullnames a path that does not exist.os.chmodsucceeds and changes nothing, andst_modecarries no permission bits. - Errors are WASI's numbers. A missing path is
[Errno 44](ENOENT), where Linux says[Errno 2]. Under a read-only grant, removing a path raisesPermissionErroreven when the path does not exist. - At most 4 GiB of memory. wasm32 addresses 65,536 pages of 64 KiB. A request past that, or past the runner's
memory_byteslimit, raisesMemoryError. Linear algebra needs about 16 MiB more than the program holds: OpenBLAS maps a buffer at its first call, and if the limit leaves no room it ends the call with exit status 1 instead of raising. - Deep recursion stops earlier than native. Recursion through Python code raises
RecursionErrorafter 40 to 250 levels, where native Python allows 1,000, because the guest's C stack is small. Recursion in C over nested data (repr,==,jsonof deeply nested structures) raises too. A few deeper cases end the call with the trap "stack overflow", such as freeing a list nested more than about 32,000 deep. A trap ends only that call. - Builtin modules have no
__file__. The packages' compiled modules (numpy's,lxml.etree,PIL._imaging) are linked in, so code that reads their__file__fails. - Compute is slower than native. numpy's own loops have no SIMD, and nothing runs on a second core; expect about 1.3 to 2.3 times native's time for numeric work.
SQLite¶
sqlite3 has native's feature set (FTS3/4/5, R-tree, Geopoly, math functions). WASI has no file locks, so SQLite uses its WASI default, a lock directory, and these facts follow from it:
- One connection holds the database, through a
<db>.lockdirectory beside it, while it writes or has a statement open. A second connection then gets "database is locked" after its busy timeout. journal_mode=walstaysdelete, unless the connection first setslocking_mode=exclusive.- A guest stopped inside a transaction leaves
<db>.lockand a hot journal. In a reused root, remove the lock directory and SQLite rolls the journal back. A call withroot=Truenever sees a stale lock, since its root is fresh. - A database in a read-only folder opens only through a URI that takes no lock, such as
file:<path>?immutable=1withuri=True, because the lock is a directory made beside it. - SQLite's own randomness (
random(), temporary file names) is seeded from the time in whole seconds, since the guest has no/dev/urandom. Usesecrets,uuid.uuid4()oros.urandomfor identifiers. - SQLite's math functions follow the guest's C library, so their last digits can differ from native's.
- No
load_extension, since WASI has nodlopen.sqlite3.threadsafetyis 0. - sqlite3 over a mount is not supported: it needs ranged writes and locking.
Packages that are not in the set¶
The set is fixed at build time. See Packages for what it carries.
- No
pipin the guest. Packages are compiled in or carried in the read-only stdlib zip; nothing is installed at run time. Pure-Python code of your own can be imported from a read-only grant throughimport_path=. - No pyarrow, so no Parquet. CSV, JSON, Excel and pickle work.
- No pytz.
zoneinforeads the zip's tzdata instead. - No
cssselectand nolxml_html_clean, solxml.cssselectandlxml.html.cleanraise lxml's ownImportError, as natively without them. - No SQLite extensions (
load_extension), as above. - No C extension of your own. A compiled module must be linked into
python.wasmwhen it is built; the guest cannot load one.
On Windows¶
A writable grant answers as Windows does; a mount answers the same on every host.
os.renameof a folder onto a file replaces the file on a Windows grant. A mount never does, and answersENOTDIR, as a Linux or macOS grant does.- A Windows grant cannot remove or rename a file the guest holds open, so
tempfile.TemporaryFileand anos.replaceinside aNamedTemporaryFile'swithfail there. A mount allows both. - A Windows grant cannot make a name with a colon, and opening a folder for reading there gives
OSErrorEINVALwhere other hosts giveIsADirectoryError. - Opening a missing path with a trailing slash raises
NotADirectoryErroron Windows,FileNotFoundErroron Linux andOSErrorEINVALon macOS. - A guest with a writable grant or a private root can delete a file the host gave the read-only attribute.
What the stdlib zip means¶
The standard library and the packages' Python code are bytecode in one read-only zip, python-stdlib.zip, seen by the guest at /usr/local/lib/python315.zip. That keeps the set small and the start fast, and it has these consequences:
- Tracebacks into the stdlib show no source line.
inspect.getsource,linecache,pdbandpydochave no source for it. os.path.existsof a stdlib module's__file__is false, and the zip's path cannot be listed.- Tracebacks and warnings name a module by its full zip path, such as
/usr/local/lib/python315.zip/pandas/core/frame.py. importlib.metadatagives each package's name, version and entry points;files()givesNone, because the zip keeps noRECORD.importlib.resources.files()on a package in the zip gives the runtime's own reader, not azipfile.Path. It lacksroot,at,glob,rgloband a few more ofzipfile.Path's members.- The packages' own
testsfolders are not in the zip. - The snapshot runs only beside the exact stdlib zip it was built against, because zipimport's file offsets are part of the snapshot.
See also Troubleshooting and The security model.