Skip to Main Content
 

Major Digest Home First look: Mojo 1.0 mixes Python and Rust - Major Digest

First look: Mojo 1.0 mixes Python and Rust

First look: Mojo 1.0 mixes Python and Rust
Credit: Info World

Back in 2023, Chris Lattner, creator of LLVM, and his team at Modular unveiled a new language called Mojo. Its syntax resembled Python, but it compiled to machine-native code and offered memory-safety features akin to Rust. It also offered cross-compatibility with existing Python programs, one of many hints that Mojo aimed to capture the math, stats, and machine learning segment of Python developers.

Now in 2026, the first beta version of Mojo 1.0 is out, and with that the shape of the language is far clearer than before. Most crucially: Mojo is not a drop-in replacement for Python. It still features Python-esque syntax and uses many of Python’s concepts, but is unmistakably headed in its own direction. As of 1.0 and beyond, Mojo aims to be a systems language with precise control over memory and strong types, while sporting convenience features inspired by higher-level languages.

Mojo basics

Mojo syntax resembles Python at first glance. The use of indents instead of braces to delineate blocks, common keywords (def for functions, etc.), how control flow is handled (if/else/while/for), exceptions, and type annotations will all be familiar to Python developers.

Where Mojo breaks with Python, and stakes out its own territory, starts with how values are handled in variables. Variables have strong types, either assigned through annotations or inferred automatically from their first assignment. If you set a to equal 1, you cannot set it to "Greetings earthlings" later. (In Python, the objects themselves are strongly typed but the names used to refer to them do not have types.)

Mojo further breaks from Python by adding a concept found in Rust: ownership of values. Instead of runtime garbage collection, Mojo uses ownership to track the lifetimes of objects at compile time.

To indicate you want to transfer ownership of a value, you use the “transfer sigil” syntax:

a = [1,2,3]
b = a^

The ^ indicates we’re moving ownership of the contents of a into b. For data types that aren’t implicitly copyable, like containers, we can use transfer of ownership. Or we can make a copy explicitly:

a = [1,2,3]
b = a.copy()

Variables that reference other values, like an element in a list, are copied if they are implicitly copyable. But you can use the ref keyword to take a reference rather than make a copy:

a = [1,2,3]
b = a[1] # copy; integers are implicitly copyable
b+=1 # changes only value of b
ref c = a[1] # reference
c+=1 # changes value stored in a[1]

Mojo also offers pointer types, whereas Python has no such thing in the language definition. You can create four kinds of pointers in Mojo, depending on how much control you need:

  • A regular Pointer points to any value it doesn’t own.
  • OwnedPointer points to a single value, which it owns.
  • ArcPointer is like an OwnedPointer but it’s reference-counted, so it can point to objects that potentially have other ArcPointers pointing to them.
  • UnsafePointer can point to anything, including uninitialized memory or multiple values (like an array in C). The idea is to use other pointer types whenever you can, and avoid UnsafePointer unless you absolutely need it.

As with Rust, any Mojo code that doesn’t follow the rules for type descriptions, ownership, and borrowing doesn’t compile.

Mojo types and values

Like Python, Mojo offers several built-in common data types. Unlike Python, they more directly correspond to high-performance, machine-level types. Mojo offers signed and unsigned integers in various bit widths, up to 64 bits, and floating-point numbers in the same range of sizes. All these types can be used in SIMD-accelerated vectors.

By contrast, Python integers can theoretically be of any size, but they don’t map directly to hardware integers, so they operate more slowly. And while Python floating-point numbers are machine-level 64-bit floats, they’re wrapped as Python objects, so they too incur performance overhead.

Mojo also offers Python-like boolean values and the list, dictionary, and set container types. Plus it adds an Optional type, which is a value that can hold a particular type of value or None, along with a Rust-like .or_else() method to obtain a default value instead of raising an error.

For working with linear algebra and multidimensional arrays, Mojo offers a layout package as part of its standard library. This includes two tensor types, the older LayoutTensor and the newer TileTensor type. The data type is declared separately from the layout, separating the concerns of data storage and data access. Layouts cover not only the dimensional shape of the data, but also things like strided access or whether the layout is row-based or column-based.

Mojo structs vs. Python classes

Whereas Python has classes, Mojo has structs. Mojo structs are defined in much the same way as Python classes, and they have many of the same behaviors:

struct Point:
    var x: Int
    var y: Int
    def __init__(out self, x: Int, y: Int):
        self.x = x
        self.y = y

The same struct can be more succinctly defined as shown below, in much the same way as Python’s dataclasses:

@fieldwise_init
struct Point:
    var x: Int
    var y: Int

By default, Mojo structs don’t support copy or move operations. Those have to be defined by adding “traits” to the struct (another nod to Rust concepts):

struct Point(Copyable):

Traits are also used to grant common behaviors across structs, since Mojo’s structs don’t have inheritance behaviors the way Python classes do. Mojo structs also lack Python’s other dynamic qualities: fields in a struct all have to be laid out ahead of time and type-defined.

Because Mojo more directly exposes machine-level types and behaviors than Python, structs can take advantage of those things. The RegisterPassable trait for a struct, for instance, allows the created type to be passed in machine registers for speed, provided it conforms to some key behaviors.

Mojo error types and exceptions

In Python, errors are propagated up the program stack as exceptions, instead of being returned as values. This means error handling works along a different path than normal program flow.

Mojo has what looks like a similar mechanism. You raise errors, and you intercept them with try/except/else/finally blocks. However, Mojo handles errors differently under the hood. In Mojo, errors are essentially values, and raising them doesn’t involve unwinding the program stack. This keeps the runtime overhead for error-checking to a minimum.

The default error type is a simple string, but you can use any struct type as an error value if you want to propagate additional information with the error. However, the Mojo compiler does not permit you to catch more than one kind of error type in a single try block. The common Python pattern of try:/except ThisError:/except ThatError:/except Exception: doesn’t exist in Mojo. This ensures that each type of error that can be raised is given distinct logical treatment.

Metaprogramming in Mojo

Python’s dynamism and runtime flexibility mean there’s little need for the metaprogramming features, like macros, that show up in other languages. The trade-off is that such flexibility comes at the cost of performance.

Mojo is more akin to Rust or C++ in that it offers compile-time metaprogramming — ways to define behaviors that are checked at compile time instead of runtime. The comptime keyword lets you define values (essentially compile-time constants), unroll loops (for faster loop execution), or invoke blocks of code to be generated based on compile-time conditions (as per #ifdef in C):

comptime if enable_tpu():
    use_tpu()
else:
    use_cpu()

Mojo also allows functions to be given their own compile-time conditions by way of parameters:

def advance_by[amount:Int](x: Int) -> Int:
    return x+amount

def main():
    comptime by_five = advance_by[5]
    n = by_five(5) 
    # n will be 10
    m = advance_by[10](2)
    # m will be 12

The parameter (in square brackets) is provided to the function at compile time. We use a comptime statement to create a new function object based on the parameters supplied, and we call that. Or, as in the line m = advance_by[10](2), we just call the function directly and provide a value known at compile time. This syntax also can be used to generate functions that are “datatype-agnostic”:

def advance_by[dt:DType](x:Scalar[dt], y:Scalar[dt]) -> Scalar[dt]:
    return x+y

DType is Mojo’s built-in namespace for data types, so this function accepts two scalar variables (float16, int32, etc.) as long as they are the same type.

Parameters also can be given default values, same as regular arguments for a function, or work with a variable number of parameters (as long as they’re all the same register-passable type).

Another compile-time feature, “constraints,” lets you define conditions for calling functions or creating structs at runtime:

def add_to_nonzero[x: Int]() -> where x >=0:
    ...

struct Box[size: Int where size>0]:
    ...

Constraints are verified at compile time, so any violation of them throws a compiler error (rather than a runtime error).

The same syntax for constraints and parameters can be used to generate generics:

def analyze[T: Comparable & Copyable](values:List[T]) -> List[T]:
    ...

A function with this signature would take in a list of values that are of a certain type T declared at compile time, and return a list of values of the same type. However, that type would have to support the Comparable and Copyable traits.

Mojo’s reflection features allow you to write code that performs compile-time actions on its own structure. As of this writing Mojo’s reflection support is limited, but can be used with traits to provide behaviors that work across struct types without needing to account directly for their design.

GPU support in Mojo

In most programming languages, Python included, GPU support isn’t part of the language as such. Rather, it’s part of whatever library you might use that supports GPU computation (for instance, CuPy).

By contrast, Mojo’s standard library has a gpu package that exposes programming APIs specifically for GPUs. Mojo’s programming model for GPUs lets you write functions that work with values that support the DevicePassable trait — integers and floats, typically — and return the results by storing them in a memory buffer passed as an argument.

Unlike Python toolkits such as Numba, Mojo doesn’t provide a way to do this automatically for a given function, for instance by way of a decorator. The Mojo function has to be GPU-friendly in its design, and additional boilerplate is needed to set up the GPU connection, compile the function on the GPU, run it, and retrieve the results. But the documentation provides detailed guidance for using GPU support properly, including how to avoid race conditions between GPU operations.

Python interop in Mojo

A major feature of Mojo’s earlier versions was the ability to call Python from Mojo and vice versa, as a way to allow Mojo to make use of the Python package ecosystem. Mojo 1.0 preserves this feature, along with the original mechanisms for it:

  • When Mojo calls Python, it invokes the CPython runtime to do so. In essence Mojo is just spinning up a CPython instance and using it as a dynamically linked library.
  • When Python calls Mojo, it loads the Mojo code as a Python module, similar to how Python uses C/C++ or any code that exposes a C-compatible FFI. A Mojo module can declare external bindings that Python can recognize and use.

The interop between Mojo and Python has the same limitations as interop between Python and C. Mojo and Python types must be converted in both directions, and the cost of making function calls in either direction isn’t trivial. That means the best uses of Mojo and Python together would be for operations where most of the work can be done in Mojo, with minimal calls across the language divide.

Mojo’s mojo

Any new programming language faces barriers. The biggest is finding an audience as a driver for growth and further development. Mojo’s target audience appears to be current Python and Rust users who have issues with their respective languages — Python’s performance, Rust’s complexity — and want a better alternative.

Another obstacle: Right now there’s no automatic migration path from Rust or Python to a pure-Mojo codebase. And if Rust and Python can make progress on their respective issues (although it seems more likely that Python will get faster than Rust will get simpler), Mojo will have even more work cut out for it.

Obstacles aside, Mojo’s direct syntax, machine-native speed, and future-looking features, like GPU-based programming, are appealing. It’s entirely possible that those features will bring Mojo a following all its own.

Sources:
Published: