Run JavaScript/WASM in Python: high-level SpiderMonkey bindings to Python with PythonMonkey

Will Pringle
6 min readJul 28, 2023

Announcing PythonMonkey’s alpha release — use Python code in JavaScript and vice versa with ease and virtually no performance loss!

Play around with it on a Google Colab: https://colab.research.google.com/drive/1INshyn0gNMgULQVtXlQWK1QuDGwdgSGZ?usp=sharing

Check out the documentation: https://docs.pythonmonkey.io/

Introduction & Goals

PythonMonkey (GitHub Link) is a Python library used for interoperation between Python and JavaScript built using Mozilla’s SpiderMonkey JavaScript engine. It will enable JavaScript libraries to be used seamlessly in Python code and vice versa — without any significant performance penalties. For instance, it’ll be possible to call Python packages like NumPy from within a JavaScript library, or use NPM packages like crypto-js directly from Python. Also, executing WebAssembly modules in Python becomes trivial using the WebAssembly API and engine from SpiderMonkey. Additionally, developers could use PythonMonkey to refactor a slow “hot loop” written in Python to execute in JS instead, taking advantage of SpiderMonkey’s just-in-time compiler for near-native speed.

PythonMonkey also ships with PMJS, a JavaScript runtime environment like Node.js that supports calling Python libraries from JavaScript.

Here are a few simple code examples:

Below is a ‘hello world’ example which demonstrates a string generated from JavaScript being returned to a Python context:

>>> import pythonmonkey as pm
>>> hello = pm.eval(" 'Hello World'.toUpperCase(); ")
>>> print(hello)
'HELLO WORLD'

This more involved example demonstrates passing the Python print function as an argument to a JavaScript function and then calling that JavaScript function from Python:

>>> import pythonmonkey as pm
>>> hello = pm.eval("(func) => { func('Hello World!')}")
>>> hello(print)
Hello World!

This example uses pmjs to execute a JavaScript file that uses Python’s print function (This can be executed with pmjs main.js):
main.js

const pyPrint = python.eval("print");
pyPrint("Hello, World!"); // this outputs "Hello, World!"

Here are a few examples of PythonMonkey’s module system in use:

PythonMonkey will allow developers to easily port their JavaScript libraries to Python, without suffering the costly burden of rewriting their libraries in Python and maintaining the ports. JavaScript is also ideal for highly asynchronous work loads, whereas Python is not. At Distributive, we intend to use this library to execute our complex dcp-client library, which is written in JS, and enables distributed computing on the web stack.

Requiring a JavaScript file from Python:
my-javascript-module.js

exports.sayHello = () => { console.log('hello, world') };

main.py

import pythonmonkey as pm
test = pm.require('./my-javascript-module');
test.sayHello() # this prints hello, world

Loading a Python CommonJS module from JavaScript:
my-python-module.py

def getStringLength(s):
return len(s)

exports['getStringLength'] = getStringLength

my-javascript-module.js

const { getStringLength } = require('./my-python-module');

function printStringLength(s) {
console.log(`String: "${s}" has a length of ${getStringLength(s)}`);
}

module.exports = { printStringLength, };

main.py

import pythonmonkey as pm
test = pm.require('./my-javascript-module');
test.printStringLength("Hello, world!") # String: "Hello, world!" has a length of 13

Calling WebAssembly function in Python:

PythonMonkey also leverages other SpiderMonkey features such as its WebAssembly (WASM) engine which allows Python to run untrusted WASM code in a sandbox from a variety of languages such as C, C++, Rust, and others.

Check out the following YouTube tutorials on using WebAssembly in Python with PythonMonkey:

Calling a WebAssembly function in Python:

import asyncio
import pythonmonkey as pm

async def async_fn():
# read the factorial.wasm binary file
file = open('factorial.wasm', 'rb')
wasm_bytes = bytearray(file.read())

# instantiate the WebAssembly code
WebAssembly = pm.eval('WebAssembly')
wasm_fact = await WebAssembly.instantiate(wasm_bytes, {})

# return the "fac" factorial function from the wasm module
return wasm_fact.instance.exports.fac;

# await the promise which returns the factorial WebAssembly function
factorial = asyncio.run(async_fn())

# execute WebAssembly code in Python!
print(factorial(4)) # this outputs "24.0" since factorial(4) == 24
print(factorial(5)) # this outputs "120.0"
print(factorial(6)) # this outputs "720.0"

For more examples, check out our examples repository on GitHub and a repository for a sample fullstack app that uses the CryptoJS NPM package in Python.

Implementation and Roadmap

PythonMonkey is designed to share immutable backing stores whenever possible. This approach is vital to control memory consumption and eliminate memory-copy overhead when moving large strings between JS and Python. Other data types are managed using ‘proxy’ data structures that understand how to talk to the underlying data. For example, when a Python dictionary is passed to a JavaScript function, a ‘proxy’ object is created. This acts as an intermediary, enabling JavaScript to read from or write to the Python dictionary. Functions also operate similarly with a proxy wrapper. Intrinsics like Boolean, Number, null, undefined are passed by value.

PythonMonkey’s roadmap includes a number of features and improvements to expand its usability, such as importing Python modules in JavaScript using esm syntax, XMLHttpRequest, implementing a standalone event loop without relying on Python’s, and support for Node.js APIs such as fs, path, process, which would allow Python to use NPM packages like express.js and socket.io. Another proposed goal further down the roadmap is to expand PMJS into a a fully integrated Node.js environment which could act as a drop-in replacement for Node.js that also has the ability to use Python packages from JavaScript.

With these planned enhancements, PythonMonkey and PMJS aim to offer a fully integrated Python-JS environment for developers. We are looking for contributors to PythonMonkey to help add these features; if you’re interested, please check out the project’s GitHub page.

A Future Challenge

A new Python proposal, PEP 623, plans to remove legacy strings, a necessary feature PythonMonkey depends on to efficiently pass string data back and forth. Legacy strings allow for a data buffer from anywhere; hence, it can point directly to a JSString’s buffer. Without them, PythonMonkey will need to make a copy of the JSString’s buffer, leading to significant time and memory inefficiencies.

Related Projects

Several projects exist already for running JavaScript within Python, such as JS2PY, PyV8, and Metacall.

JS2Py is implemented entirely in Python, a characteristic that might seem attractive as it eliminates the need for substantial engines like V8 or SpiderMonkey. However, this approach brings its own set of challenges. Not utilizing an existing JavaScript engine denies JS2Py the opportunity to benefit from robust, continuously updated, and proven codebases that engines like V8 or SpiderMonkey offer in browsers used by millions of people every day. Furthermore, JS2Py misses out on functionalities such as a WASM engine, support for the latest JavaScript specification (ECMA-262), and a powerful JIT, which come built-in with these engines. JavaScript Promises and Async/Await, which are used extensively in modern asynchronous JS programming, are also missing in JS2Py but are available in PythonMonkey. Finally, being written in Python, JS2Py faces performance constraints that aren’t present in SpiderMonkey; the SunSpider JavaScript benchmark reports a 1162.5× speedup from using PythonMonkey over JS2Py.

PyV8, and Cloudflare’s modern implementation of it, are Python wrappers for Google’s V8 JavaScript engine bindings. This means it operates at a lower level than PythonMonkey and doesn’t support event loop functionality such as JavaScript’s promises and async/await.

Metacall is an extensible, embeddable and interoperable cross-platform polyglot runtime that interoperates with a number of programming languages such as JavaScript, Python, Ruby, Rust, C#, Java and more. This means Metacall can be used to interoperate between Python and JavaScript similarly to PythonMonkey. However, the extensive supported languages Metacall supports comes at a cost, and requires additional software to be installed on a system beyond a Python package for it to operate. Additionally, Metacall makes copies of data it passes between Python and JavaScript instead of passing by reference like how PythonMonkey does leading to performance impacts.

While alternative projects have similarities with PythonMonkey’s model, they fall short of PythonMonkey’s proposed interoperability, ease of use, and speed.

Try it out

Play around with it on a Google Collab: https://colab.research.google.com/drive/1INshyn0gNMgULQVtXlQWK1QuDGwdgSGZ?usp=sharing

If you’re interested in trying out PythonMonkey or PMJS on your system, you can either install via pip:
$ pip install pythonmonkey
or follow the build instructions on our GitHub page: https://github.com/Distributive-Network/PythonMonkey. Note: PythonMonkey requires npm to be installed on a system during its build process.

Stay tuned for more PythonMonkey blog posts

We’ll continue to make more blog posts as we implement features in PythonMonkey. If you’re interested in learning more and staying up to date, subscribe to our newsletter for updates, join our discord https://discord.gg/8KvEGZas9R, and check out the project’s website: https://pythonmonkey.io/

If you’re interested in contributing to PythonMonkey, check out the project on GitHub: https://github.com/Distributive-Network/PythonMonkey

And feel free to get in touch with the PythonMonkey team!
pm@distributive.network

NOTE — I did not create PythonMonkey!

--

--