For the complete Mojo documentation index, see llms.txt. Markdown versions of all pages are available by appending .md to any URL (e.g. /docs/manual/basics.md).
Mojo compound statements reference
A compound statement has a header and a body. The header ends
with : and is followed by an indented block with the body.
The body can contain simple statements, other compound statements, or both.
if condition: # Header
do_something() # Body
The body must be indented more than the header. The first body statement sets the indentation for the rest of the body:
if condition:
do_something()
do_more() # Error because statement has excess indentation
If statements
An if statement executes a block conditionally:
if x > 0:
print("positive")
elif x < 0:
print("negative")
elif x == 0:
print("zero")
else:
print("you should never get here")
Conditions are evaluated in order. Add as many as needed.
The first true condition runs its block, and the statement exits.
The else block runs if no condition is true.
When the body is a single simple statement, you can write it on a single line, although many style guides discourage this.
if x > 0: print("positive")
Common shortcuts from other languages won't work in Mojo:
x > 0 and print("positive") # Error because 'None' isn't truthy
print("positive") if x > 0 else pass
# Error because 'pass' isn't an expression
While loops
The while loop repeats its body while a condition is true:
var count = 0
while count < 10:
print(count)
count += 1
Use break to exit the loop early and continue to skip to the next
iteration:
while True:
var item = get_next()
if item is None:
break # Exit loop if no more items
if not is_valid(item):
continue # Skip invalid items
process(item) # Only runs for valid items
For loops
The for statement iterates over a sequence:
for item in items:
process(item)
for i in range(10): # [0, 10)
print(i)
To support iteration, a sequence must implement __iter__() and
__next__(). A for loop desugars to a while loop that uses these
methods.
Destructuring works directly in the loop target. This lets you unpack tuple
elements as you iterate. In this example, each item in pairs is unpacked
into key and value for every iteration:
for key, value in pairs: # For example, [("a", 1), ("b", 2), ...]
print(key, value)
Loops and else clauses
An optional else clause runs when the loop exits normally.
It does not run if the loop exits with break:
var found = False
for item in items:
if item == target:
found = True
break
else:
print("not found") # Only runs if break was never hit
Both for and while loops support else.
Error handling
A try statement executes code that may raise errors.
try:
result = risky()
except e:
handle(e)
Structure
Each try statement requires at least one except or finally clause:
try:
operation()
except e:
handle_error(e) # Runs if an error occurs
else:
on_success() # Runs only if no error occurred
finally:
cleanup() # Always runs
Execution proceeds in a fixed order:
- The
tryblock runs first. - If an error occurs, the matching
exceptblock runs. - If no error occurs, the
elseblock (if present) runs after thetryblock. - If included, a
finallyblock always runs last.
Error binding
Bind the error to a name with except name:
try:
risky()
except e:
print(e) # e is the caught error
Without a binding, the error is caught but not accessible. There is no default error variable. This is useful when you want to respond to an error state without needing the error details:
try:
risky()
except:
print("something went wrong")
Typed errors
When a function declares a specific error type with raises ErrorType,
the bound variable's type is inferred:
@fieldwise_init
struct NetworkError:
var message: String
var code: Int
def fetch() raises NetworkError -> String:
raise NetworkError("HTCPCP", 418) # See RFC 2324 *
try:
result = fetch()
except e: # e is inferred as `NetworkError`
print(e.message) # Known types support direct field access
print(e.code) # `.code` and `.message` only work because `e`
# is a known to be `NetworkError`
A try block handles one error type. The compiler raises an
error if code in the try block can raise more than one
error type.
Context managers
A with statement manages resources using context managers. Context
managers define setup (__enter__) and cleanup (__exit__) operations.
The cleanup always runs when the block exits, even if an error occurs:
with open("file.txt") as f:
content = f.read()
# File is closed here, even if an error occurred
Multiple context managers can share a single with statement:
with open("input.txt") as f_in, open("output.txt", "w") as f_out:
f_out.write(f_in.read())
This is equivalent to nested with statements.
How context managers work
When a with block is entered, __enter__() is called on the context
manager expression. The result is bound to the as target if present. A
context manager that defines only __enter__() is valid; __exit__() is
optional. When the block exits, __exit__() is called if it exists, even
if an error occurs.
A minimal custom context manager:
struct Scope(ImplicitlyCopyable):
var label: String
def __init__(out self, label: String):
self.label = label
def __enter__(self) -> Self:
# perform setup tasks
print("entering", self.label)
return self
def __exit__(self):
# perform cleanup tasks
print("exiting", self.label)
def main():
with Scope("setup") as s:
print("inside", s.label)
# entering setup
# inside setup
# exiting setup
__enter__ returns self so the as target binds to the manager. The
ImplicitlyCopyable conformance lets the compiler return self by value.
Compile-time control flow
comptime if and comptime for run at compile time. The condition
or sequence must be a compile-time value or expression.
Use them to generate code based on compile-time conditions. You
cannot use runtime values in comptime statements.
comptime if
comptime if selects a branch at compile time, pruning the unselected
branches. Only the selected branch appears in the compiled program.
from std.sys import size_of
comptime if size_of[Int]() == 8:
print("64-bit")
else:
print("Probably 32-bit")
The condition must be a compile-time expression. In this example,
runtime_value is not available at compile time, so the code errors
during compilation:
comptime if runtime_value > 0: # Error because 'comptime if' requires
pass # compile-time evaluation
comptime if supports elif and else like the regular if statement.
comptime for
comptime for unrolls a loop at compile time. Each iteration is
compiled as separate code. This creates a bigger binary but improves
runtime performance by eliminating loop overhead and enabling further
optimizations.
comptime for i in range(3):
print(i) # Compiled as: print(0); print(1); print(2)
Use comptime for to generate repeated code patterns or iterate over
compile-time sequences.
Scopes
Each compound statement body creates a new scope. Variables declared inside a body are not visible outside it:
if condition:
var x = 10
print(x) # Error: x is not in scope
with statement variables bound with as are scoped to the with block:
with open("file.txt") as f:
data = f.read()
# f is not accessible here
Nested functions create their own scope and can capture variables from enclosing functions with capture lists:
def outer():
count = 0
def inner() {mut count}: # Capture count by mutable reference
count += 1 # Updates captured count
inner()
print(count) # 1