Pyscript/Pyodide and JS Object Passing

Published August 21, 2022

Tags: Python PyScript Pyodide Javascript

A question I've been seeing quite a bit over in the Unofficial PyScript Community Discord is: How do you pass objects back and forth between JavaScript and PyScript/Pyodide? So I've created recipies below for passing objects back and forth between JavaScript and Python; the specifics are somewhat different depending on whether we're working in PyScript or directly in Pyodide, so both options are illustrated below.

Currently, you can:

  • ✅ Pass objects from JavaScript to Python running in PyScript
  • ✅ Pass objects from JavaScript Python running in Pyodide
  • ✅ Pass objects from Python running in Pyodide to JavaScript
  • ⚠️ Pass objects from Python running in PyScript to JavaScript, with a little extra work. See the commentary and live demo with the code sample below.

For our purposes, an 'object' is anything that can be bound to a variable (a number, string, object, function, etc). Also, recall that the import js or from js import ... in Pyodide gets objects from the JavaScript globalThis scope, so keep the rules of JavaScript variable scoping in mind.

(At time of writing, the most current versions are PyScript Version 2022.06.1 and Pyodide v21. Both are changing fast, so if you're reading this in the far future, check the documentation and forums for new best procedures.)

JavaScript to Python (PyScript)

We can use the simple from js import ... to import JavaScript objects directly into PyScript.

Javascript to Python (PyScript)

1
2
3
4
5
6
7
8
<script>
    name = "Jeff" //A JS variable
    // Define a JS Function
    function addTwoNumbers(x, y){
        return x + y;
    }
</script>
<py-script>
 8
 9
10
    # Import and use JS function in Python
    from js import name, addTwoNumbers, console
    console.log("Hello " + name + ".Adding 1 and 2 in Javascript: " + str(addTwoNumbers(1, 2)))
11
</py-script>

JavaScript to Python (Pyodide)

We can also use from js import ... to import JavaScript objects directly into Python in Pyodide. The syntax is identical to the PyScript example above - the <py-script> calls the runPython function for us (among other things).

Javascript to Python (Pyodide)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<script>
    name = "Jeff" //A JS variable
    // Define a JS Function
    function addTwoNumbers(x, y){
        return x + y;
    }

    async function main() {
        let pyodide = await loadPyodide();
        result = pyodide.runPython(`
            # Import and use JS function in Python
            from js import name, addTwoNumbers, console
            console.log("Hello " + name + ".Adding 1 and 2 in Javascript: " + str(addTwoNumbers(1, 2)))
        `);
    }
    main();
</script>

Python (Pyodide) to JavaScript

One we've initialized the Pyodide runtime, the JS object pyodide.globals is a mapping that represents the global Python namespace. We can use the get() method to retrieve an object from this mapping and make use of it in JavaScript.

Python (Pyodide) to JavaScript

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<script type="module">
    const pyodideRuntime = await loadPyodide();
    pyodideRuntime.runPython(`
        name = "Jeff" # A Python variable
        # Define a Python function
        def multiplyTwoNumbers(x, y):
            return (x * y)
    `);
    // Access and call it in JavaScript
    let mult = pyodideRuntime.globals.get('multiplyTwoNumbers');
    console.log("Multiplying 2 and 3 in Python: " + mult(2,3));
    console.log("You're welcome, " + pyodideRuntime.globals.get('name'))
</script>

Python (PyScript) to JavaScript

Since PyScript doesn't export its instance of Pyodide and only one instance of Pyodide can be running in a browser window at a time, there isn't currently a way for Javascript to access Objects defined inside PyScript tags "directly".

However, I've found a workaround using JavaScript's eval() function, which executes a string as code much like Python's eval(). First, we create a JS function createObject which takes an object and a string, then uses eval() to bind that string as a variable to that object. By calling this function from PyScript (where we have access to the Pyodide global namespace), we can bind JavaScript variables to Python objects without having direct access to that global namespace.

1
2
3
4
5
6
7
8
9
<script>
    function createObject(object, variableName){
        //Bind a variable whose name is the string variableName
        // to the object called 'object'
        let execString = variableName + " = object"
        console.log("Running `" + execString + "`");
        eval(execString)
    }
</script>

This takes a Python Object and creates a variable pointing to it in the JavaScript global scope. So what if we made a JavaScript variable point at... the Python global namespace?

exportGlobals.py

from js import createObject
from pyodide import create_proxy
createObject(create_proxy(globals()), "pyodideGlobals")

This, amazingly, just works. All Python global variables are now accessible at in JavaScript with the syntax pyodideGlobals.get('myVariableName')

Let's see an example running live. The three buttons below print the values of the variables x, y, and z respectively, as looked up in the Python global namespace. Use the REPL to set the values of those variables, and see how JavaScript goes from seeing them as "undefined" to their value in PyScript.

I've pre-populated an example line in the REPL for you. Click the '' or press shift-enter to run the current REPL line.

x = "Hello, world!"

#button-output



buttons.js

1
<script>
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
buttonOutput = document.getElementById("button-output")

document.getElementById("x").addEventListener("click", () => {
    buttonOutput.innerHTML += pyodideGlobals.get('x') + "<br>"
});

document.getElementById("y").addEventListener("click", () => {
    buttonOutput.innerHTML += pyodideGlobals.get('y') + "<br>"
});

document.getElementById("z").addEventListener("click", () => {
    buttonOutput.innerHTML += pyodideGlobals.get('z') + "<br>"
});
15
<script>

A Deeper Dive

We don't have to export the entire Python global namespace as an object if we don't want to. The example below shows exporting a single list and a lambda function as JavaScript variables, using the same createObject function above.

Note that the names of the JavaScript variable and the Python variable don't have to be similar/identical/different - I've named them similarly ('names' and 'names_js', 'mutliplier' and 'multiplier_js') for readability.

Python (PyScript) to JavaScript
10
11
12
13
14
15
16
17
import js
from pyodide import create_proxy, to_js

names = ["Jeff Glass"]
js.createObject(create_proxy(names), "names_js")

multiplier = lambda z: z * 2
js.createObject(create_proxy(multiplier), "multiplier_js")
19
</py-script>

The code above binds the JavaScript variable names_js to a PyProxy of the Python list names, and the JavaScript variables multiplier_js to a PyProxy for the Python lambda function multiplier.

Of course, this means we have to use the createObject function to "export" the objects from Python before we can use them in JavaScript. But this may be preferred for your use case.

With those objects created, we can refer to/call them like any other JS objects. To see this, let's add two buttons: one that references our function and list from within JavaScript ("use-python-objects"), and one that adds some names to our list so we can see it change ("add-name").

20
21
22
23
24
<py-env>
    - faker
</py-env>

<py-script>
25
26
27
28
29
30
31
32
33
34
35
from pyodide import create_proxy
from faker import Faker

fake = Faker()

def add_a_name(*args, **kwargs):
    new_name = fake.name()
    console.log(f"Adding {new_name} to names")
    names.append(new_name)

Element("add-name").element.addEventListener("click", create_proxy(add_a_name))
36
37
</py-script>
 
38
<script>
39
40
41
42
43
44
45
46
47
48
    document.getElementById("use-python-objects").addEventListener("click", () => {
        console.log("Displaying contents of Python list 'names', calling Python function 'multiplier'")
        el = document.getElementById("output")
        el.innerHTML = '' //Clear contents of output
        for (const name of names_js){
            el.innerHTML += "Name: " + name + "<br\>";
        };
        number = Math.floor(Math.random() * 10) + 1 //random between 1 and 10
        el.innerHTML += number + " times two is " + multiplier_js(number) + "<br\>";
    });
49
<script>

Python (PyScript) Individual Objects to JavaScript Demo

The code in the preceding section is running live on this page. Click "Add Name to List" to append a new name (provided by the Faker library) to the list names; click "Use Python Objects" to reference that list (and the multiplier function) and display the results in the green box. Open your browser's development console to see additional output.

#output:


- faker

Viewing globals()

Since we have a reference to the PyScript global namespace, we can also just view its contents from JavaScript. And again so we can see it really changing, let's add a button that creates new Python objects with random names using exec():

displayGlobals.js

1
<script>
2
3
4
5
document.getElementById("printGlobals").addEventListener("click", () => {
    console.warn("Clicked print globals")
    document.getElementById("globals").innerHTML = pyodideGlobals;
});
6
<script>

makeNewObjects.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from random import choice, randint
import string
from js import document
from pyodide import create_proxy, to_js

def makePythonObject(*args, **kwargs):
    name = ''.join([choice(string.ascii_lowercase) for _ in range(5)])
    value = randint(0, 100)
    exec_string = f"global {name}\n{name} = {value}"
    exec(exec_string)

document.getElementById("makeObject").addEventListener("click", create_proxy(makePythonObject))

Click the Print Globals button to see the Python global objects visible from JavaScript; click the Make Python Variable to make a new Python variable with a 5-letter name (then click Print Globals again to see it). Since this shares a global namespace with the rest of the PyScript code on this page, you may also see variables like 'x', 'y', and 'z' from the example above.

#globals: