Post

HN
Hacker News (Newest)

Mojo's Not (Yet) Python

Mojo's not (yet) Python March 12, 2026 by Phil Eaton mojo python programming languages 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 " {sitename} 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 My blog 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 My blog Hello world! Let's try the same thing with PyPy. $ pypy3 build.py Wrote docs/index.html $ cat docs/index.html My blog 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. About the author Phil is the founder of The Consensus. Before this, he contributed to Postgres products at EnterpriseDB, cofounded and led marketing at TigerBeetle, and was an engineering manager at Oracle. He runs the Software Internals Discord, the Software Internals Email Book Club, and co-runs NYC Systems. @eatonphil Notice a mistake? Have a question or comment? Write to the editor .