Mojo language basics
This page provides an overview of the Mojo language.
If you know Python, then a lot of Mojo code will look familiar. However, Mojo incorporates features like static type checking, memory safety, next-generation compiler technologies, and more. As such, Mojo also has a lot in common with languages like C++ and Rust.
If you prefer to learn by doing, follow the Get started with Mojo tutorial. You'll install the Magic CLI, create a Mojo project and write your first Mojo program.
On this page, we'll introduce the essential Mojo syntax, so you can start coding quickly and understand other Mojo code you encounter. Subsequent sections in the Mojo Manual dive deeper into these topics, and links are provided below as appropriate.
Let's get started! 🔥
Hello world
Here's the traditional "Hello world" program in Mojo:
def main():
print("Hello, world!")
def main():
print("Hello, world!")
Every Mojo program must include a function named main()
as the entry point.
We'll talk more about functions soon, but for now it's enough to know that
you can write def main():
followed by an indented function body.
The print()
statement does what you'd expect, printing its arguments to
the standard output.
Variables
In Mojo, you can declare a variable by simply assigning a value to a new named variable:
def main():
x = 10
y = x * x
print(y)
def main():
x = 10
y = x * x
print(y)
You can also explicitly declare variables with the var
keyword:
var x = 10
var x = 10
When declaring a variable with var
, you can also declare a variable type, with
or without an assignment:
def main():
var x: Int = 10
var sum: Int
sum = x + x
def main():
var x: Int = 10
var sum: Int
sum = x + x
Both implicitly declared and explicitly declared variables are statically typed: that is, the type is set at compile time, and doesn't change at runtime. If you don't specify a type, Mojo uses the type of the first value assigned to the variable.
x = 10
x = "Foo" # Error: Cannot convert "StringLiteral" value to "Int"
x = 10
x = "Foo" # Error: Cannot convert "StringLiteral" value to "Int"
For more details, see the page about variables.
Blocks and statements
Code blocks such as functions, conditions, and loops are defined with a colon followed by indented lines. For example:
def loop():
for x in range(5):
if x % 2 == 0:
print(x)
def loop():
for x in range(5):
if x % 2 == 0:
print(x)
You can use any number of spaces or tabs for your indentation (we prefer 4 spaces).
All code statements in Mojo end with a newline. However, statements can span multiple lines if you indent the following lines. For example, this long string spans two lines:
def print_line():
long_text = "This is a long line of text that is a lot easier to read if"
" it is broken up across two lines instead of one long line."
print(long_text)
def print_line():
long_text = "This is a long line of text that is a lot easier to read if"
" it is broken up across two lines instead of one long line."
print(long_text)
And you can chain function calls across lines:
def print_hello():
text = String(",")
.join("Hello", " world!")
print(text)
def print_hello():
text = String(",")
.join("Hello", " world!")
print(text)
For more information on loops and conditional statements, see Control flow.
Functions
Mojo functions can be declared with either the def
or fn
keyword.
There are only a few differences between the two styles. For example, the
following function works with either def
or fn
:
def greet(name: String) -> String:
return "Hello, " + name + "!"
def greet(name: String) -> String:
return "Hello, " + name + "!"
The fn
declaration has a few restrictions that a def
declaration doesn't:
-
The argument type and return types are optional in a
def
function but required in afn
function. So the previous function could also be written as:def greet(name):
return "Hello, " + name + "!"def greet(name):
return "Hello, " + name + "!"Generally, specifying types is good practice, so most examples in this manual include types, regardless of whether they're declared with
def
orfn
. -
If an
fn
function can raise an error, it needs to be declared with theraises
keyword:fn greet(name: String) raises:
if (name == ""):
raise Error("Name is empty")
return "Hello, " + name + "!"fn greet(name: String) raises:
if (name == ""):
raise Error("Name is empty")
return "Hello, " + name + "!"Any
def
function can raise an error.
If you don't specify a type for an argument or return value, it's assigned the
object
type, a special type that that can represent different types of values.
This allows for some runtime dynamism, but also means that
the function might fail at runtime if it receives the wrong type.
For more details, see the page about functions.
Value ownership and argument mutability
If you're wondering whether function arguments are passed by value or passed by reference, the short answer is: by default, arguments are passed by immutable reference.
The longer short answer is that Mojo allows you to specify for each argument
whether it should be passed by value (as owned
), or whether it should be
passed by reference (as read
for an immutable reference, or as mut
for a mutable reference).
This feature is entwined with Mojo's value ownership model, which protects you from memory errors by ensuring that only one variable "owns" a value at any given time (but allowing other variables to receive a reference to it). Ownership then ensures that the value is destroyed when the lifetime of the owner ends (and there are no outstanding references).
For a more complete answer, see the section on value ownership.
Code comments
You can create a one-line comment using the hash #
symbol:
# This is a comment. The Mojo compiler ignores this line.
# This is a comment. The Mojo compiler ignores this line.
Comments may also follow some code:
var message = "Hello, World!" # This is also a valid comment
var message = "Hello, World!" # This is also a valid comment
API documentation comments are enclosed in triple quotes. For example:
fn print(x: String):
"""Prints a string.
Args:
x: The string to print.
"""
...
fn print(x: String):
"""Prints a string.
Args:
x: The string to print.
"""
...
Documenting your code with these kinds of comments (known as "docstrings")
is a topic we've yet to fully specify, but you can generate an API reference
from docstrings using the mojo doc
command.
Structs
You can build high-level abstractions for types (or "objects") as a struct
.
A struct
in Mojo is similar to a class
in Python: they both support
methods, fields, operator overloading, decorators for metaprogramming, and so
on. 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 Python-style classes in the future.)
For example, here's a basic struct:
struct MyPair:
var first: Int
var second: Int
fn __init__(out self, first: Int, second: Int):
self.first = first
self.second = second
fn __copyinit__(out self, existing other):
self.first = existing.first
self.second = existing.second
def dump(self):
print(self.first, self.second)
struct MyPair:
var first: Int
var second: Int
fn __init__(out self, first: Int, second: Int):
self.first = first
self.second = second
fn __copyinit__(out self, existing other):
self.first = existing.first
self.second = existing.second
def dump(self):
print(self.first, self.second)
And here's how you can use it:
def use_mypair():
var mine = MyPair(2, 4)
mine.dump()
def use_mypair():
var mine = MyPair(2, 4)
mine.dump()
Note that some functions are declared with fn
function, while the dump()
function is declared with def
. In general, you can use either form in a
struct.
The MyPair
struct contains two special methods, __init__()
, the constructor,
and __copyinit__()
, the copy constructor. Lifecycle methods like this
control how a struct is created, copied, moved, and destroyed.
For most simple types, you don't need to write the lifecycle methods. You can
use the @value
decorator to generate the boilerplate code for you. So the
MyPair
struct can be simplified to this:
@value
struct MyPair:
var first: Int
var second: Int
def dump(self):
print(self.first, self.second)
@value
struct MyPair:
var first: Int
var second: Int
def dump(self):
print(self.first, self.second)
For more details, see the page about structs.
Traits
A trait is like a template of characteristics for a struct. If you want to create a struct with the characteristics defined in a trait, you must implement each characteristic (such as each method). Each characteristic in a trait is a "requirement" for the struct, and when your struct implements all of the requirements, it's said to "conform" to the trait.
Using traits allows you to write generic functions that can accept any type that conforms to a trait, rather than accept only specific types.
For example, here's how you can create a trait:
trait SomeTrait:
fn required_method(self, x: Int): ...
trait SomeTrait:
fn required_method(self, x: Int): ...
The three dots following the method signature are Mojo syntax indicating that the method is not implemented.
Here's a trait that conforms to SomeTrait
:
@value
struct SomeStruct(SomeTrait):
fn required_method(self, x: Int):
print("hello traits", x)
@value
struct SomeStruct(SomeTrait):
fn required_method(self, x: Int):
print("hello traits", x)
Then, here's a function that uses the trait as an argument type (instead of the struct type):
fn fun_with_traits[T: SomeTrait](x: T):
x.required_method(42)
fn use_trait_function():
var thing = SomeStruct()
fun_with_traits(thing)
fn fun_with_traits[T: SomeTrait](x: T):
x.required_method(42)
fn use_trait_function():
var thing = SomeStruct()
fun_with_traits(thing)
You'll see traits used in a lot of APIs provided by Mojo's standard library. For
example, Mojo's collection types like List
and Dict
can store any type that
conforms to the CollectionElement
trait. You can specify the type when you
create a collection:
my_list = List[Float64]()
my_list = List[Float64]()
Without traits, the x
argument in fun_with_traits()
would have to declare a
specific type that implements required_method()
, such as SomeStruct
(but then the function would accept only that type). With traits, the function
can accept any type for x
as long as it conforms to (it "implements")
SomeTrait
. Thus, fun_with_traits()
is known as a "generic function" because
it accepts a generalized type instead of a specific type.
For more details, see the page about traits.
Parameterization
In Mojo, a parameter is a compile-time variable that becomes a runtime constant, and it's declared in square brackets on a function or struct. Parameters allow for compile-time metaprogramming, which means you can generate or modify code at compile time.
Many other languages use "parameter" and "argument" interchangeably, so be aware that when we say things like "parameter" and "parametric function," we're talking about these compile-time parameters. Whereas, a function "argument" is a runtime value that's declared in parentheses.
Parameterization is a complex topic that's covered in much more detail in the Metaprogramming section, but we want to break the ice just a little bit here. To get you started, let's look at a parametric function:
def repeat[count: Int](msg: String):
@parameter # evaluate the following for loop at compile time
for i in range(count):
print(msg)
def repeat[count: Int](msg: String):
@parameter # evaluate the following for loop at compile time
for i in range(count):
print(msg)
This function has one parameter of type Int
and one argument of type
String
. To call the function, you need to specify both the parameter and the
argument:
defining call_repeat():
repeat[3]("Hello")
# Prints "Hello" 3 times
defining call_repeat():
repeat[3]("Hello")
# Prints "Hello" 3 times
By specifying count
as a parameter, the Mojo compiler is able to optimize the
function because this value is guaranteed to not change at runtime. And the
@parameter
decorator in the code tells the compiler to evaluate the for
loop
at compile time, not runtime.
The compiler effectively generates a unique version of the repeat()
function
that repeats the message only 3 times. This makes the code more performant
because there's less to compute at runtime.
Similarly, you can define a struct with parameters, which effectively allows you to define variants of that type at compile-time, depending on the parameter values.
For more detail on parameters, see the section on Metaprogramming.
Python integration
Mojo supports the ability to import Python modules as-is, so you can leverage existing Python code right away.
For example, here's how you can import and use NumPy:
from python import Python
def main():
var np = Python.import_module("numpy")
var ar = np.arange(15).reshape(3, 5)
print(ar)
print(ar.shape)
from python import Python
def main():
var np = Python.import_module("numpy")
var ar = np.arange(15).reshape(3, 5)
print(ar)
print(ar.shape)
You must have the Python module (such as numpy
) installed in the environment
where you're using Mojo. You can install Python packages into your virtual
environment using Magic or Conda.
For more details, see the page on Python integration.
Next steps
Hopefully this page has given you enough information to start experimenting with Mojo, but this is only touching the surface of what's available in Mojo.
If you're in the mood to read more, continue through each page of this Mojo Manual—the next page from here is Functions.
Otherwise, here are some other resources to check out:
-
See Get started with Mojo for a hands-on tutorial that will get you up and running with Mojo.
-
If you want to experiment with some code, clone the Mojo repo to try our code examples:
git clone https://github.com/modular/mojo.git
git clone https://github.com/modular/mojo.git
-
To see all the available Mojo APIs, check out the Mojo standard library reference.
Was this page helpful?
Thank you! We'll create more content like this.
Thank you for helping us improve!