Skip to content

Types equivalence

Momtchil Momtchev edited this page Nov 23, 2022 · 6 revisions

Automatic

When passing objects between the interpreters:

  • JavaScript to Python conversion is automatic and by copying
  • Python objects are passed by reference to JavaScript as a PyObject
    • they can be automatically converted to JavaScript when the context permits it - as governed by the Symbol.toPrimitive() rules - for example in +a + 2 if a is a PyObject containing a number, it will be automatically converted to a JavaScript number
    • they can be explicitly converted to JavaScript by calling .toJS()
    • they can be directly interacted with it - for example if a PyObject is a Python list, then JavaScript can directly call its append method

Functions also can be freely passed between the two interpreters. Every time a cross-language function is called, it is executed by the corresponding interpreter. Their arguments will be converted according to the same rules.

Explicit

JavaScript to Python

A PyObject can be created by calling PyObject.fromJS():

const py = PyObject.fromJS({ name: 'label', value: 42 });
  • A number becomes an int when it has no decimal part or a float when it has one
  • A BigInt becomes an int
  • A bool becomes a bool
  • Undefined and null become None
  • A string becomes an unicode string
  • An array becomes a list
  • An object becomes a dictionary
  • A PyObject or a proxified PyObject is always passed by reference and reverts to its Python type
  • A Buffer becomes a bytearray
  • A JS function (including a native function) becomes a callable pymport.js_function

This conversion is by copy.

Additionally, there are explicit constructors by Python type:

  • PyObject.int()
  • PyObject.float()
  • PyObject.string()
  • PyObject.dict()
  • PyObject.list()
  • PyObject.tuple()
  • PyObject.set()
  • PyObject.frozenSet()
  • PyObject.slice()
  • PyObject.bytes()
  • PyObject.bytearray()
  • PyObject.memoryview()
  • PyObject.func()

All of these copy the data except PyObject.memoryview() which creates a view for the data which becomes referenced in Python. This is the only case in which the Python GC can hold a V8 object. The V8 object won't be freed until Python has released it.

[New in 1.2] When creating a Python callable from a JavaScript function, the JavaScript function will be called from Python with all of its arguments being PyObjects. If calling through a proxified method, all of its arguments will also be proxified.

Python to JavaScript

JavaScript can directly interact with a PyObject - refer to Using PyObjects directly and Using proxified PyObjects.

Additionally, a native JavaScript object can be created by calling .toJS() (or .valueOf() which is equivalent):

  • A float becomes a number
  • An int becomes a number if it is in the safe integer number range or a BigInt otherwise
  • A bool becomes a bool
  • None becomes null
  • An unicode string becomes a string
  • A list, a tuple, a set or a frozenset becomes an Array
  • A dict becomes an object
  • Any object implementing the Buffer Protocol - bytes, bytearray or a memoryview - becomes a Buffer. The memory referenced by the Buffer is a copy of the Python memory
  • A callable becomes a native (binary) function that references a trampoline which converts arguments and invokes the Python function
  • A module becomes an object
  • Everything else remains a PyObject

Using toJS() on the module object

Direct conversion of the Python module object itself to a JavaScript object is supported too, but this is the least compatible mode, as some Python constructs cannot be expressed in JS:

// np is a plain JS object whose properties are the numpy methods
const np = pymport('numpy').toJS();
const a = np.arange(15).reshape(3, 5);

In theory, this should combine the performance of the direct access mode with the comfort of the proxify interface. In reality many Python-specific features will be impossible to express. In particular, in this example, there is no way to make the chaining of numpy methods work unless the returned value is converted to JavaScript at each step - which will have a huge performance impact.

proxify is a much better way to have an interface with a native feel.

Functions

Since 1.1 functions can be freely passed between Python and JavaScript.

As when calling Python functions, argument conversion from JavaScript to Python is always automatic.

[New in 1.2] Conversion from Python to JavaScript is automatic only when it can be deduced from the context (in a + 1, a will be converted to number) according to the rules governing Symbol.toPrimitive. In all other cases, toJS() must be called.

[New in 1.2] If the function is passed to Python through a proxified method, it will receive proxified PyObject arguments.

If JavaScript calls a Python function with an unsupported type (Symbol for example), the call will throw.

Here are a few examples of different ways of getting a Python function and calling it:

// a is a callable PyObject, can be called with a.call()
const a = np.get('ones');

// b is a proxified callable PyObject, can be called with b()
// All objects returned by b() will also be proxified
const b = proxify(np).ones;

// c is a native code JS function, can be called with c()
const c = np.get('ones').toJS();

All three of those can also be used as a Python object to be passed as an argument to a Python function.

When passing a JavaScript function to Python, the resulting object has a special Python type pymport.js_function, that cannot be constructed in any other way. It is a Python callable that is otherwise indistinguishable from a Python function.

Exceptions

When the Python code raises an exception, it can be catched from JavaScript. The Python exception will be transformed to a normal JavaScript Error object containing the Python error message prefixed with Python exception. This object will be extended with an additional attribute, pythonTrace which will be a PyObject containing the Python traceback. This traceback can be processed with the usual Python traceback module.

If a JavaScript callback passed to Python throws, the Python code will receive a a generic Exception object containing the JavaScript error message. If Python does not handle this error, the error will eventually propagate back to the calling JavaScript code where it will be a JavaScript Error object containing a pythonTrace with the Python part of the stack.