Mojo language basics

A short introduction to the Mojo language basics.

Mojo is a powerful programming language that’s primarily designed for high-performance systems programming, so it has a lot in common with other systems languages like Rust and C++. Yet, Mojo is also designed to become a superset of Python, so a lot of language features and concepts you might know from Python translate nicely to Mojo.

For example, if you’re in a REPL environment or Jupyter notebook (like this document), you can run top-level code just like Python:

print("Hello Mojo!")
Hello Mojo!

You don’t normally see that with other systems programming languages.

Mojo preserves Python’s dynamic features and language syntax, and it even allows you to import and run code from Python packages. However, it’s important to know that Mojo is an entirely new language, not just a new implementation of Python with syntax sugar. Mojo takes the Python language to a whole new level, with systems programming features, strong type-checking, memory safety, next-generation compiler technologies, and more. Yet, it’s still designed to be a simple language that’s useful for general-purpose programming.

This page provides a gentle introduction to the Mojo language, and requires only a little programming experience. So let’s get started!

If you’re an experienced systems programmer and want a deep dive into the language, check out the Mojo programming manual.

Language basics

First and foremost, Mojo is a compiled language and a lot of its performance and memory-safety features are derived from that fact. Mojo code can be ahead-of-time (AOT) or just-in-time (JIT) compiled.

Like other compiled languages, Mojo programs (.mojo or .🔥 files) require a main() function as the entry point to the program. For example:

fn main():
    var x: Int = 1
    x += 1
    print(x)

If you know Python, you might have expected the function name to be def main() instead of fn main(). Both actually work in Mojo, but using fn behaves a bit differently, as we’ll discuss below.

Of course, if you’re building a Mojo module (an API library), not a Mojo program, then your file doesn’t need a main() function (because it will be imported by other programs that do have one).

Note: When you’re writing code in a .mojo/.🔥 file, you can’t run top-level code as shown on this page—all code in a Mojo program or module must be encased in a function or struct. However, top-level code does work in a REPL or Jupyter notebook (such as the notebook for this page).

Now let’s explain the code in this main() function.

Syntax and semantics

This is simple: Mojo supports (or will support) all of Python’s syntax and semantics. If you’re not familiar with Python syntax, there are a ton of great resources online that can teach you.

For example, like Python, Mojo uses line breaks and indentation to define code blocks (not curly braces), and Mojo supports all of Python’s control-flow syntax such as if conditions and for loops.

However, Mojo is still a work in progress, so there are some things from Python that aren’t implemented in Mojo yet (see the Mojo roadmap). All the missing Python features will arrive in time, but Mojo already includes many features and capabilities beyond what’s available in Python.

As such, the following sections will focus on some of the language features that are unique to Mojo (compared to Python).

Functions

Mojo functions can be declared with either fn (shown above) or def (as in Python). The fn declaration enforces strongly-typed and memory-safe behaviors, while def provides Python-style dynamic behaviors.

Both fn and def functions have their value, and it’s important that you learn them both. However, for the purposes of this introduction, we’re going to focus on fn functions only. For much more detail about both, see the programming manual.

In the following sections, you’ll learn how fn functions enforce strongly-typed and memory-safe behaviors in your code.

Variables

You can declare variables (such as x in the above main() function) with var to create a mutable value, or with let to create an immutable value.

If you change var to let in the main() function above and run it, you’ll get a compiler error like this:

error: Expression [15]:7:5: expression must be mutable for in-place operator destination
    x += 1
    ^

That’s because let makes the value immutable, so you can’t increment it.

And if you delete var completely, you’ll get an error because fn functions require explicit variable declarations (unlike Python-style def functions).

Finally, notice that the x variable has an explicit Int type specification. Declaring the type is not required for variables in fn, but it is desirable sometimes. If you omit it, Mojo infers the type, as shown here:

fn do_math():
    let x: Int = 1
    let y = 2
    print(x + y)

do_math()
3

Function arguments and returns

Although types aren’t required for variables declared in the function body, they are required for arguments and return values for an fn function.

For example, here’s how to declare Int as the type for function arguments and the return value:

fn add(x: Int, y: Int) -> Int:
    return x + y

z = add(1, 2)
print(z)
3

Optional arguments and keyword arguments

You can also specify argument default values (also known as optional arguments), and pass values with keyword argument names. For example:

fn pow(base: Int, exp: Int = 2) -> Int:
    return base ** exp

# Uses default value for `exp`
z = pow(3)
print(z)

# Uses keyword argument names (with order reversed)
z = pow(exp=3, base=2)
print(z)
9
8

Note: Mojo currently includes only partial support for keyword arguments, so some features such as keyword-only arguments and variadic keyword arguments (e.g. **kwargs) are not supported yet.

Argument mutability and ownership

Mojo supports full value semantics and enforces memory safety with a robust value ownership model (similar to the Rust borrow checker). So the following is a quick introduction to you can share references to values through function arguments.

Notice that, above, add() doesn’t modify x or y, it only reads the values. In fact, as written, the function cannot modify them because fn arguments are immutable references, by default.

In terms of argument conventions, this is called “borrowing,” and although it’s the default for fn functions, you can make it explicit with the borrowed declaration like this (this behaves exactly the same as the add() above):

fn add(borrowed x: Int, borrowed y: Int) -> Int:
    return x + y

If you want the arguments to be mutable, you need to declare the argument convention as inout. This means that changes made to the arguments inside the function are visible outside the function.

For example, this function is able to modify the original variables:

fn add_inout(inout x: Int, inout y: Int) -> Int:
    x += 1
    y += 1
    return x + y

var a = 1
var b = 2
c = add_inout(a, b)
print(a)
print(b)
print(c)
2
3
5

Another option is to declare the argument as owned, which provides the function full ownership of the value (it’s mutable and guaranteed unique). This way, the function can modify the value and not worry about affecting variables outside the function. For example:

fn set_fire(owned text: String) -> String:
    text += "🔥"
    return text

fn mojo():
    let a: String = "mojo"
    let b = set_fire(a)
    print(a)
    print(b)

mojo()
mojo
mojo🔥

In this case, Mojo makes a copy of a and passes it as the text argument. The original a string is still alive and well.

However, if you want to give the function ownership of the value and do not want to make a copy (which can be an expensive operation for some types), then you can add the ^ “transfer” operator when you pass a to the function. The transfer operator effectively destroys the local variable name—any attempt to call upon it later causes a compiler error.

Try it above by changing the call to set_fire() to look like this:

    let b = set_fire(a^)

You’ll now get an error because the transfer operator effectively destroys the a variable, so when the following print() function tries to use a, that variable isn’t initialized anymore.

If you delete print(a), then it works fine.

These argument conventions are designed to provide systems programmers with total control for memory optimizations while ensuring safe access and timely deallocations—the Mojo compiler ensures that no two variables have mutable access to the same value at the same time, and the lifetime of each value is well-defined to strictly prevent any memory errors such as “use-after-free” and “double-free.”

Note: Currently, Mojo always makes a copy when a function returns a value.

Structures

You can build high-level abstractions for types (or “objects”) in a struct. A struct in Mojo is similar to a class in Python: they both support methods, fields, operator overloading, decorators for metaprogramming, etc. However, Mojo structs are completely static—they are bound at compile-time, so they do not allow dynamic dispatch or any runtime changes to the structure. (Mojo will also support classes in the future.)

For example, here’s a basic struct:

struct MyPair:
    var first: Int
    var second: Int

    fn __init__(inout self, first: Int, second: Int):
        self.first = first
        self.second = second
    
    fn dump(self):
        print(self.first, self.second)

And here’s how you can use it:

let mine = MyPair(2, 4)
mine.dump()
2 4

If you’re familiar with Python, then the __init__() method and the self argument should be familiar to you. If you’re not familiar with Python, then notice that, when we call dump(), we don’t actually pass a value for the self argument. The value for self is automatically provided with the current instance of the struct (it’s used similar to the this name used in some other languages to refer to the current instance of the object/type).

For much more detail about structs and other special methods like __init__() (also known as “dunder” methods), see the programming manual.

Python integration

Although Mojo is still a work in progress and is not a full superset of Python yet, we’ve built a mechanism to import Python modules as-is, so you can leverage existing Python code right away. Under the hood, this mechanism uses the CPython interpreter to run Python code, and thus it works seamlessly with all Python modules today.

For example, here’s how you can import and use NumPy (you must have Python numpy installed):

from python import Python

let np = Python.import_module("numpy")

ar = np.arange(15).reshape(3, 5)
print(ar)
print(ar.shape)
[[ 0  1  2  3  4]
 [ 5  6  7  8  9]
 [10 11 12 13 14]]
(3, 5)

Note: Mojo is not a feature-complete superset of Python yet. So, you can’t always copy Python code and run it in Mojo. For more details on our plans, please refer to the Mojo roadmap and sharp edges.

Caution: When you install Mojo, the installer searches your system for a version of Python to use with Mojo, and adds the path to the modular.cfg config file. If you change your Python version or switch virtual environments, Mojo will then be looking at the wrong Python library, which can cause problems such as errors when you import Python packages (Mojo says only An error occurred in Python—this is a separate known issue). The current solution is to override Mojo’s path to the Python library, using the MOJO_PYTHON_LIBRARY environment variable. For instructions on how to find and set this path, see this related issue.

Next steps

We hope this page covered enough of the basics to get you started. It’s intentionally brief, so if you want more detail about any of the topics touched upon here, check out the Mojo programming manual.

Note: The Mojo SDK is still in early development. Some things are still rough, but you can expect constant changes and improvements to both the language and tools. Please see the known issues and report any other issues on GitHub.