17. Python interoperability

As mentioned in the beginning of this book, Mojo is very close to Python in terms of syntax. However, Mojo and Python are semantically two completely different languages, though they share very similar syntax. Python has a huge ecosystem behind it, built over many decades. Mojo allows the developer to leverage this huge ecosystem, with its Python integration capability.

The integration of Mojo with Python is built on a key insight that from a practical point of view, all Python objects can be represented with a single type. In Mojo it is represented by the PythonObject struct. Mojo uses the actual CPython interpreter for interoperability. The usage of CPython in Mojo enables high fidelity integration with Python, and ensures that the Python objects behave as expected.

17.1. Importing a Python module

Mojo has module named python that encapsulates all of the Python integration. Within the python module, there is the facade object that provides the entry point for many of the Python integration capabilities.

Importing a Python module is as simple as using the Python.import_module method call, passing it the module name. In case a module does not exist, ensure that the module is installed using the pip or equivalent command. Mojo also supports Python virtual environments.

The following code listing provides an example on how to import Python modules and how to use objects from Python.

    from python import Python
    var difflib = Python.import_module("difflib")
    var list1 = Python.list("One", "Two", "Three", "Four")
    var list2 = Python.list("One", "Two", "Three")
    var differ = difflib.HtmlDiff() # Get instance of HtmlDiff class.
    var diff = differ.make_file(list1, list2)
    print(diff)

17.2. Evaluating Python expressions

You can evaluate Python expressions using Python.evaluate method.

    print(Python.evaluate("1+2"))

Since Python treats functions as first class, you can even use Python.evaluate to access Python’s built-in functions.

    var str_fn = Python.evaluate("str")
    print(str_fn("ABC") <= str_fn("XYZ"))

Note in the above code listing, how transparently we can transfer Mojo string literals to the Python function, and how seamlessly Mojo operators work over Python objects, invoking the right Python dunder methods. One important constraint is that only PythonObject struct can be passed to and from Python. In the case of Mojo string literals, PythonObject has a constructor that takes StringLiteral, resulting in implicit conversion from StringLiteral to a PythonObject instance. PythonObject has many such constructors for the various Mojo built-in types. PythonObject also implements many traits that enable it to be used within functions such as len, int, str and so on.

17.3. Other useful Python functions

You can use native Python objects like list and dict using corresponding static methods exposed by Python. Python also exposes type function from Python and can be used to determine the underlying type of the PythonObject struct.

    var a_list = Python.list()
    a_list.append("First element")
    a_list.append("Second element")
    print(a_list)
    print(Python.type(a_list))

17.4. Calling a Mojo function from Python

You can also call a Mojo function from Python. In fact, Mojo allows you to call Mojo functions without having to even compile the Mojo code. This is possible through dynamic compilation and loading of Mojo code.

mymath.mojo
from python import PythonObject
from python.bindings import PythonModuleBuilder
from os import abort

@export
fn PyInit_mymath() -> PythonObject: # Function name must be PyInit_<module_name>
    try:
        var m = PythonModuleBuilder("mymath") # Create a new Python module named "mymath"
        m.def_function[sub]("sub", docstring="Subtract two numbers") # Add the `sub` function to the module with a docstring
        return m.finalize() # Finalize the module and return it to Python
    except e:
        abort(String("Could not create module:", e)) # Can't proceed if we can't create the module

fn sub(a: PythonObject, b: PythonObject) raises -> PythonObject:
    return a - b

In the above code listing, we are defining a module named mymath that has a function sub which takes two integers and returns their difference.

There is some boilerplate required to define a Python-accessible module in Mojo. We use PythonModuleBuilder to build the module. We then add the sub function to the module using the def_function method, which takes the function name, function documentation, and a reference to the actual function itself as parameters. Finally, we call finalize method to create the module.

We can call this function from Python as shown in the following code listing.

import mojo.importer # Required to import Mojo files
import mymath

print(mymath.sub(10, 4))

Please note that in the above code listing, we import mojo.importer to enable importing of Mojo modules in Python. The mojo.importer finds the relevant Mojo module, compiles it if necessary, and loads it into Python, making the sub function available for use. We can then import the mymath module and call the sub function defined in it, passing the required arguments.