You are getting early access to this article as a subscriber. Your support makes articles like this possible. Thank you.
Mojo is a new proprietary, high-level systems programming language from Chris Lattner. The home page of Mojo says it's “Pythonic”. A user quote on the homepage says “Mojo is Python++. It will be, when complete, a strict superset of the Python language.” And the homepage talks about Python interoperability.
As both a big programming languages nerd and someone who writes a lot of Python scripts, I have been curious to try out Mojo for some time. I assumed it was something similar to PyPy or Cython where you could write Python code and import some Python libraries and unless you did things that directly relied on the runtime itself (e.g. FFI calls), existing code would all basically work.
On a Ubuntu 24.04 machine install these and we'll take a look.
$ sudo apt-get install -y python3-pip cython3 pypy3 hyperfine
$ pip3 install --break-system-packages mojo
$ python3 --version
Python 3.12.3
$ cython3 --version
Cython version 3.0.8
$ pypy3 --version
Python 3.9.18 (7.3.15+dfsg-1build3, Apr 01 2024, 03:12:48)
[PyPy 7.3.15 with GCC 13.2.0]
$ mojo --version
Mojo 0.26.1.0 (156d3ac6)
While there are many similarities between Python and Mojo at the syntax level, and while the goal of Mojo might indeed be to eventually be a superset of Python, I would say that it is basically not compatible with Python. I don't think I was the only person unclear about this since, for example, the Wikipedia page for Mojo currently has a Python-looking example of a sub function that doesn't actually compile. (Perhaps it used to compile under a previous version of Mojo.)
$ cat foo.mojo
def sub(x, y):
res = x - y
return res
def main():
sub(3, 1)
$ mojo foo.mojo
/tmp/trymojo/foo.mojo:1:9: error: argument type must be specified
def sub(x, y):
^
/tmp/trymojo/foo.mojo:1:12: error: argument type must be specified
def sub(x, y):
^
/usr/local/bin/mojo: error: failed to parse the provided Mojo source module
I assumed Mojo would be something like TypeScript or OCaml where types are largely optional. And again that's what the Wikipedia page sort of suggests. But in reality types are required for arguments and return values. A version of foo.mojo that compiles is:
$ cat foo.mojo
def sub(x: Int, y: Int) -> Int:
res = x - y
return res
def main():
print(sub(3, 1))
$ mojo foo.mojo
2
Let's write a simple static site generator in Python to see how much of it is compatible with Mojo. Call it build.py.
import os
import pathlib
import shutil
TEMPLATES_DIR = "templates"
TEMPLATE_EXTENSION = ".tmpl.html"
OUT_DIR = "docs"
SITENAME = "My blog"
# Clean up out directory.
try:
shutil.rmtree(OUT_DIR)
except Exception as e:
pass
pathlib.Path(OUT_DIR).mkdir(parents=True)
# Iterate over templates to write them out.
for fname in os.listdir(TEMPLATES_DIR):
if not fname.endswith(TEMPLATE_EXTENSION):
continue
with open(os.path.join(TEMPLATES_DIR, fname)) as f:
tmpl = f.read()
outpath = os.path.join(OUT_DIR, fname[:-len(TEMPLATE_EXTENSION)] + ".html")
with open(outpath, "w") as f:
# Render template with variables substituted.
f.write(tmpl.format(sitename=SITENAME))
print(f"Wrote {outpath}")
Create templates/index.tmpl.html:
$ mkdir templates
$ echo "<h1>{sitename}</h1>
Hello world!" > templates/index.tmpl.html
And run the Python script.
$ python3 build.py
Wrote docs/index.html
And see what it generated.
$ cat docs/index.html
<h1>My blog</h1>
Hello world!
Let's try the same thing with Cython.
$ cython3 --embed build.py
$ gcc -o build build.c $(python3-config --includes --ldflags --embed)
$ ./build
Wrote docs/index.html
$ cat docs/index.html
<h1>My blog</h1>
Hello world!
Let's try the same thing with PyPy.
$ pypy3 build.py
Wrote docs/index.html
$ cat docs/index.html
<h1>My blog</h1>
Hello world!
Alright, let's see how Mojo runs this.
$ mojo build.py
/usr/local/bin/mojo: error: no such command 'build.py'
Ok, no problem. Looking at mojo --help maybe we can mojo run build.py.
$ mojo run build.py
/usr/local/bin/mojo: error: cannot open 'build.py', since it does not appear to be a Mojo file (it does not end in '.mojo' or '.🔥')
Alright so we cannot run any existing Python file directly from Mojo without first renaming it. Let's rename it.
$ cp build.py build.mojo
$ mojo run build.mojo
/tmp/trymojo/build.mojo:5:1: error: expressions are not supported at the file scope
TEMPLATES_DIR = "templates"
^
/tmp/trymojo/build.mojo:6:1: error: expressions are not supported at the file scope
TEMPLATE_EXTENSION = ".tmpl.html"
^
/tmp/trymojo/build.mojo:7:1: error: expressions are not supported at the file scope
OUT_DIR = "docs"
^
/tmp/trymojo/build.mojo:8:1: error: expressions are not supported at the file scope
SITENAME = "My blog"
^
/tmp/trymojo/build.mojo:11:1: error: 'try' must be contained in a function but is contained in a file scope.
try:
^
/tmp/trymojo/build.mojo:12:3: error: expressions are not supported at the file scope
shutil.rmtree(OUT_DIR)
^
/tmp/trymojo/build.mojo:13:18: error: expected ':' after 'except'
except Exception as e:
^
/tmp/trymojo/build.mojo:3:8: error: unable to locate module 'shutil'
import shutil
^
/usr/local/bin/mojo: error: failed to parse the provided Mojo source module
So two more differences from Python are that the standard library package shutil doesn't exist and that we can't have top-level expressions.
Let's rewrite build.mojo to work around these differences. We'll put all the code in a main function and we'll call that function the usual Pythonic way if __name__ == “__main__”. Also we'll just drop shutil and assume the user removes and creates the directory.
import os
import pathlib
def main():
TEMPLATES_DIR = "templates"
TEMPLATE_EXTENSION = ".tmpl.html"
OUT_DIR = "docs"
SITENAME = "My blog"
# Assume that OUT_DIR is an empty, existing directory.
# Iterate over templates to write them out.
for fname in os.listdir(TEMPLATES_DIR):
if not fname.endswith(TEMPLATE_EXTENSION):
continue
with open(os.path.join(TEMPLATES_DIR, fname)) as f:
tmpl = f.read()
outpath = os.path.join(OUT_DIR, fname[:-len(TEMPLATE_EXTENSION)] + ".html")
with open(outpath, "w") as f:
# Render template with variables substituted.
f.write(tmpl.format(sitename=SITENAME))
print(f"Wrote {outpath}")
if __name__ == "__main__":
main()
Let's try it out.
$ mojo run build.mojo
/tmp/trymojo/build.mojo:27:4: error: use of unknown declaration '__name__'
if __name__ == "__main__":
^~~~~~~~
/tmp/trymojo/build.mojo:17:10: error: invalid call to 'open': missing 1 required positional argument: 'mode'
with open(os.path.join(TEMPLATES_DIR, fname)) as f:
^~~~
/tmp/trymojo/build.mojo:1:1: note: function declared here
import os
^
/usr/local/bin/mojo: error: failed to parse the provided Mojo source module
Alright, it's not compatible with the __name__ check. We can just define main() and Mojo will call it for us. And then also apparently we need to add the mode flag ("r") to the open() call.
import os
import pathlib
def main():
TEMPLATES_DIR = "templates"
TEMPLATE_EXTENSION = ".tmpl.html"
OUT_DIR = "docs"
SITENAME = "My blog"
# Assume that OUT_DIR is an empty, existing directory.
# Iterate over templates to write them out.
for fname in os.listdir(TEMPLATES_DIR):
if not fname.endswith(TEMPLATE_EXTENSION):
continue
with open(os.path.join(TEMPLATES_DIR, fname), "r") as f:
tmpl = f.read()
outpath = os.path.join(OUT_DIR, fname[:-len(TEMPLATE_EXTENSION)] + ".html")
with open(outpath, "w") as f:
# Render template with variables substituted.
f.write(tmpl.format(sitename=SITENAME))
print(f"Wrote {outpath}")
And run it.
$ mojo run build.mojo
/tmp/trymojo/build.mojo:23:19: error: invalid call to 'format': unknown keyword argument: 'sitename'
f.write(tmpl.format(sitename=SITENAME))
~~~~^~~~~~~
/tmp/trymojo/build.mojo:1:1: note: function declared here
import os
^
/tmp/trymojo/build.mojo:25:12: error: expected ')' in call argument list
print(f"Wrote {outpath}")
^
/usr/local/bin/mojo: error: failed to parse the provided Mojo source module
Ah, Mojo does not yet support named format arguments. And it does not seem to support f-strings either.
At this point I don't really want to keep trying since named format arguments were a key part of the static site generator.
Now it is true that Mojo allows you to easily import existing Python code and run it. Let's rewrite build.mojo completely to do this.
from std.python import Python
def main():
Python.add_to_path(".")
Python.import_module("build")
And run it.
$ mojo run build.mojo
Wrote docs/index.html
That took quite a while, and completely failed on a cheap DigitalOcean instance (I got JIT session error: Cannot allocate memory until I upped the memory to 8GiB). But eventually it worked!
To deal with the long time this took to run I figured I could use Mojo's build command to get a binary instead.
$ mojo build build.mojo
$ ./build
#0 0x00007e5c0c1cb78b (/usr/local/lib/python3.12/dist-packages/modular/lib/libKGENCompilerRTShared.so+0x3cb78b)
#1 0x00007e5c0c1c93c6 (/usr/local/lib/python3.12/dist-packages/modular/lib/libKGENCompilerRTShared.so+0x3c93c6)
#2 0x00007e5c0c1cc397 (/usr/local/lib/python3.12/dist-packages/modular/lib/libKGENCompilerRTShared.so+0x3cc397)
#3 0x00007e5c0ba45330 (/lib/x86_64-linux-gnu/libc.so.6+0x45330)
#4 0x00006101a47c18bc std::python::_cpython::CPython::__init__() build.mojo:0:0
#5 0x00006101a47c4325 std::python::python::Python::import_module(::String$)_closure_0 build.mojo:0:0
#6 0x00007e5c0c0f8db6 KGEN_CompilerRT_GetOrCreateGlobalIndexed (/usr/local/lib/python3.12/dist-packages/modular/lib/libKGENCompilerRTShared.so+0x2f8db6)
#7 0x00006101a47c4450 std::python::python::Python::import_module(::String$) build.mojo:0:0
#8 0x00006101a47be3ab main (/tmp/build+0x13ab)
#9 0x00007e5c0ba2a1ca (/lib/x86_64-linux-gnu/libc.so.6+0x2a1ca)
#10 0x00007e5c0ba2a28b __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28b)
#11 0x00006101a47be255 _start (/tmp/build+0x1255)
Illegal instruction (core dumped)
But the behavior of this mojo build process seems sufficiently different from mojo run that I couldn't get it working on my cheap DigitalOcean machine with 8GiB memory. I switched to one of DigitalOcean's CPU-optimized instances where it promises “best-in-class processors”.
$ mojo build build.mojo
$ ./build
Wrote docs/index.html
And now things work!
Whatever the case, importantly, this isn't Mojo that reimplemented Python support when I'm importing build.py from Mojo. From the docs, “The Python code runs in a standard Python interpreter (CPython).”
Let's see Python, Cython, PyPy, and Mojo all together then. And we'll separate out compile steps to the degree that we can (PyPy has no separate compile step I'm aware of).
$ # First build the Cython binary.
$ cython3 --embed build.py
$ gcc -o buildcython build.c $(python3-config --includes --ldflags --embed)
$ # Next build the Mojo binary.
$ mojo build build.mojo -o buildmojo
$ # Last the Python bytecode.
$ python3 -m py_compile build.py
$ hyperfine \
"python3 __pycache__/build.cpython-312.pyc" \
"./buildmojo" \
"./buildcython" \
"pypy3 build.py"
Benchmark 1: python3 __pycache__/build.cpython-312.pyc
Time (mean ± σ): 35.0 ms ± 1.8 ms [User: 27.5 ms, System: 7.3 ms]
Range (min … max): 33.0 ms … 48.3 ms 81 runs
Warning: Statistical outliers were detected. Consider re-running this benchmark on a quiet system without any interferences from other programs. It might
help to use the '--warmup' or '--prepare' options.
Benchmark 2: ./buildmojo
Time (mean ± σ): 101.5 ms ± 1.7 ms [User: 75.1 ms, System: 23.3 ms]
Range (min … max): 98.7 ms … 105.1 ms 29 runs
Benchmark 3: ./buildcython
Time (mean ± σ): 39.3 ms ± 1.5 ms [User: 32.1 ms, System: 7.1 ms]
Range (min … max): 37.1 ms … 43.4 ms 70 runs
Benchmark 4: pypy3 build.py
Time (mean ± σ): 78.5 ms ± 1.3 ms [User: 55.6 ms, System: 22.6 ms]
Range (min … max): 76.6 ms … 83.1 ms 37 runs
Summary
python3 __pycache__/build.cpython-312.pyc ran
1.12 ± 0.07 times faster than ./buildcython
2.25 ± 0.12 times faster than pypy3 build.py
2.90 ± 0.16 times faster than ./buildmojo
And if we combine the build and run steps.
$ hyperfine --prepare "rm -rf __pycache__ buildcython build build.c" \
"python3 build.py" \
"mojo run build.mojo" \
"cython3 --embed build.py; gcc -o buildcython build.c -I/usr/include/python3.12 -I/usr/include/python3.12 -L/usr/lib/python3.12/config-3.12-x86_64-linux-gnu -L/usr/lib/x86_64-linux-gnu -lpython3.12 -ldl -lm; ./buildcython" \
"pypy3 build.py"
Benchmark 1: python3 build.py
Time (mean ± σ): 35.9 ms ± 1.3 ms [User: 28.3 ms, System: 7.5 ms]
Range (min … max): 33.5 ms … 39.3 ms 80 runs
Benchmark 2: mojo run build.mojo
Time (mean ± σ): 865.2 ms ± 12.7 ms [User: 706.4 ms, System: 108.5 ms]
Range (min … max): 851.1 ms … 885.0 ms 10 runs
Benchmark 3: cython3 --embed build.py; gcc -o buildcython build.c -I/usr/include/python3.12 -I/usr/include/python3.12 -L/usr/lib/python3.12/config-3.12-x86
_64-linux-gnu -L/usr/lib/x86_64-linux-gnu -lpython3.12 -ldl -lm; ./buildcython
Time (mean ± σ): 912.1 ms ± 8.6 ms [User: 766.6 ms, System: 144.5 ms]
Range (min … max): 894.4 ms … 920.0 ms 10 runs
Benchmark 4: pypy3 build.py
Time (mean ± σ): 77.0 ms ± 1.5 ms [User: 53.9 ms, System: 22.9 ms]
Range (min … max): 74.7 ms … 82.4 ms 37 runs
Summary
python3 build.py ran
2.14 ± 0.09 times faster than pypy3 build.py
24.10 ± 0.96 times faster than mojo run build.mojo
25.41 ± 0.97 times faster than cython3 --embed build.py; gcc -o buildcython build.c -I/usr/include/python3.12 -I/usr/include/python3.12 -L/usr/lib/python3.12/config-3.12-x86_64-linux-gnu -L/usr/lib/x86_64-linux-gnu -lpython3.12 -ldl -lm; ./buildcython
This script is tiny and not representative of the kind of math-heavy work Mojo is built for. At most this article shows that Mojo is not a trivial replacement for Python. It is tackling some of the problems that PyPy and Cython have also tried to tackle. But after raising a total of $380M it seems to be a whole lot better funded than PyPy anyway. (I can't easily find Cython's funding situation.)
The Mojo language is early (first released in 2023) and promising. The first-class Python FFI (which we did not take a look at in this article) is compelling. What's more, the Mojo team has indicated they intend to open-source the language eventually. The standard library is open-source already. It's an interesting stage of a language where you can easily find things to contribute to, especially if their goal is to become more compatible with Python over time.
Enjoyed this article? Subscribe for unlimited access and to help us keep producing excellent articles.