All Tutorials

Mojo Programming LanguageGitHub

Direct GPU programming without CUDA — Modular's AI systems language

Mojo AI CUDA MAX Modular GPU

1 · Foundations & Toolchain

What Mojo actually is

Mojo is a natively compiled, Python-shaped systems language designed for AI and high-performance computing. The exercises used throughout this tutorial live in the companion repo nikhil-pagote/mojolearn — clone it to run the examples locally. Three things set Mojo apart:

  • Compiled, not interpreted. mojo run JIT-compiles to native machine code; mojo build produces a standalone binary. No VM, no bytecode interpreter.
  • Python syntax, without the Python runtime. def, indentation, familiar builtins — but statically typed, with compile-time ownership tracking. Mojo looks like Python, but its execution model is closer to Rust, Swift, and C++ — see Tips for Python devs for the full list of differences.
  • Hardware-first. First-class SIMD vector types, designed from day one for performance-critical code. Python interop is pay-per-use — a compiled hello-world links no libpython at all.
Mojo is not a Python JIT. Python interop calls into CPython only when you opt in with Python.import_module. A program that never does that runs standalone.

Mojo is now open source

As of August 18, 2026 — announced at ModCon 2026, one week after the 1.0 release — Mojo's compiler, tooling, and standard library are open source under the Apache License v2.0 with LLVM Exceptions, in github.com/modular/modular. Modular's framing: "the language was incubated privately while its architecture stabilized; now Mojo's evolution continues in the open." (Modular itself was acquired by Qualcomm for roughly $3.9B, a deal that closed in late July 2026, shortly before the 1.0 release.)

The same repository also hosts MAX, which is not Apache 2.0 — using or distributing MAX is governed by the separate Modular Community License.

Running Mojo · toolchain

The exercises in this tutorial are drawn from github.com/nikhil-pagote/mojolearn — clone it to follow along. The recommended setup uses pixi with the stable mojo==1.0.0 package. Core commands:

git clone https://github.com/nikhil-pagote/mojolearn
cd mojolearn

pixi install                                     # sync env from pixi.lock

pixi run mojo run exercises/01_hello.mojo        # JIT-compile and run
pixi run mojo build exercises/01_hello.mojo -o hello  # compile to binary
./hello                                          # runs without pixi

pixi shell                                       # enter env; use mojo directly
mojo run is the inner loop for development. mojo build produces a binary you can ship — clang is required for the linking step, but not for mojo run.

Upgrading Mojo · stable channel

Mojo ships on two channels. Stable is numbered releases such as 1.0.0 — what this tutorial targets and what you want unless you have a specific reason otherwise. Nightly is pre-release builds from a separate channel, covered in the next section. Moving between versions within a channel is an upgrade; moving between channels means repointing the project at a different index.

Always start by checking what you actually have — a pixi.lock pins the toolchain, so the version in your project is often older than the one you installed globally:

pixi run mojo --version      # version inside the project env
mojo --version               # version on PATH, if pixi shell is active

There are two different upgrades, and confusing them is why people think pixi update is broken:

# 1. Move within the constraint already in pixi.toml
#    (e.g. 1.0.0 -> 1.0.3 when pinned as "mojo==1.0.*")
pixi update mojo

# 2. Cross to a version the constraint excludes.
#    pixi update will NOT do this — you must change the constraint.
pixi add "mojo==1.0.0"       # rewrites pixi.toml, regenerates pixi.lock
pixi update only moves you as far as the version specifier in pixi.toml allows; it never rewrites that specifier. Going from a 0.25.x pin to 1.0.0 is a pixi add with the new version, not an update.

On the uv/pip path the equivalent is a single flag:

uv pip install --upgrade mojo        # latest stable
uv pip install "mojo==1.0.0"         # pin explicitly
Crossing 1.0 breaks less than you'd think. Checked against the 1.0.0 compiler, only one of these is fatal:
  • Hard errorfn: "'fn' has been removed; use 'def' instead". This is the one that stops a build.
  • Deprecation warning, still compilesaliascomptime, __del____deinit__, UnsafePointerPointer, and @parameter forcomptime for.
  • Accepted silentlyInlineArray (the alias for Array survives) and capturing closures, which were not removed at all.
So a pre-1.0 codebase usually builds after deleting fn, then warns its way through the rest. Commit your pixi.lock before upgrading so you can get back. The 1.0.0 changelog is the migration checklist; every rename it lists is covered somewhere in this tutorial.

Upgrading Mojo · nightly channel

Nightly is a different channel, not a version you can reach with pixi update or --upgrade — those only ever move you within the stable channel. Reach for nightly only when you need a fix or an unreleased feature that has not landed in stable; everything in this tutorial targets stable 1.0.0.

With pixi, the channel is chosen when the project is created:

pixi init myproject \
  -c https://conda.modular.com/max-nightly/ \
  -c conda-forge
cd myproject
pixi add mojo

For a project that already exists, edit the channels list in pixi.toml — swap max for max-nightly — then re-resolve with pixi install.

With uv, point at the nightly index and allow pre-releases. Both flags are required; without --prerelease allow the resolver silently skips every nightly build and you end up back on stable:

uv pip install mojo \
  --index https://whl.modular.com/nightly/simple/ \
  --prerelease allow

To go back to stable, reinstall without the nightly index (uv) or restore the max channel (pixi). Keeping two pixi projects — one per channel — is less error-prone than switching one back and forth.

Read the matching docs. Modular publishes a separate documentation branch for nightly at mojolang.org/nightly. Checking a nightly build's behaviour against the stable 1.0.0 reference is a reliable way to lose an afternoon — APIs that this tutorial documents as current may already have moved there.

Hello World · Exercise 01

An executable .mojo file needs a def main() — there's no top-level code. File scope is for declarations only: def, struct, trait, comptime, imports. A module meant only to be imported needs no main().

Code blocks below are fragments. Statements shown at the left margin — print(...), var x = ... — belong inside def main() in a real file. Only declarations sit at file scope.
# wrong — expressions at file scope are a compile error
print("hi")
# error: expressions must not appear at file scope

# correct
def main():
    print("Hello, World!")
Hello, World!

Variables · Exercise 02

Declare with var. Type is inferred from the assigned value, or you can annotate it explicitly. Mojo has no let keyword — var is the binding form for mutable and effectively constant values.

def main():
    var x = 41            # inferred as Int
    var y: Int = 1        # explicit type
    x = x + y            # var is reassignable
    print("x =", x)      # x = 42
Gotcha: There is no let keyword. Every variable declaration uses var.

Scalar types

Beyond Int, Float64, Bool, and String, Mojo supports a full set of fixed-width numeric types:

  • Signed integers: Int (machine word), Int8, Int16, Int32, Int64, Int128, Int256
  • Unsigned integers: UInt, UInt8, UInt16, UInt32, UInt64, UInt128, UInt256
  • Floating point: Float16, Float32, Float64, BFloat16, Float8_e4m3fn
  • Other: Bool, String, Byte (alias of UInt8)
Bare literals default to specific types: var x = 42 infers Int; var f = 3.5 infers Float64.

String length gotcha

"Length" is ambiguous for UTF-8 text. Mojo makes you say which one you mean:

var s: String = "café"
print(s.byte_length())        # 5  (UTF-8 bytes)
print(s.count_codepoints())   # 4  (visible characters)
Gotcha: len(s) still compiles but warns. Prefer byte_length() or count_codepoints().

Constants: comptime

comptime declares a compile-time constant. The right-hand side is evaluated by the compiler — not at runtime — and the name cannot be reassigned:

comptime PI = 3.14

def circumference(r: Float64) -> Float64:
    return 2.0 * PI * r

def area(r: Float64) -> Float64:
    return PI * r * r
Note: comptime is the current Mojo constant syntax and replaces the older alias keyword you may see in earlier tutorials. The compiler rejects any attempt to reassign a comptime name.

Undeclared variables, scoping, and unused values

Mojo still allows variable definitions without var inside functions, but 1.0 deprecated them — each one warns "implicit declaration of 'x' is deprecated; add 'var' before the name". The scoping rule is worth understanding anyway, because it explains what var actually changes: without it, an assignment in an inner block modifies the outer binding; with it, the inner binding is a new scoped variable and the outer is untouched:

def main():
    x = 1        # implicit declaration — deprecated in 1.0, warns
    y = 1
    if True:
        x = 4    # modifies outer x (no var)
        var y = 4  # new scoped y; outer y unchanged
        print("inner x:", x)   # 4
        print("inner y:", y)   # 4
    print("outer x:", x)       # 4  (was modified)
    print("outer y:", y)       # 1  (unchanged)

Use _ to suppress unused-variable warnings. It also works in tuple unpacking:

var x, _, z = 1, 2, 3    # middle value intentionally discarded
print(x, z)               # 1  3

Backtick notation lets you use reserved words or characters that are normally illegal in identifiers:

var `var`: Int = 1
var `with space`: Int = 2

def `with#symbol`() -> Int:
    return 3

print(`var`, `with space`, `with#symbol`())   # 1  2  3

StringLiteral vs String

A string written directly in source code has type StringLiteral — a compile-time constant stored in the binary. String is the runtime heap-allocated type. The compiler converts StringLiteral → String implicitly when needed. Note that StringLiteral is parameterised by its own value, so you can't annotate with a bare StringLiteral: write StaticString, or just let it infer.

var s: StaticString = "hello"    # compile-time constant
var t: String = "hello"          # runtime, heap-allocated

def greet(name: String):
    print("Hi,", name)

greet("World")    # StringLiteral auto-converts to String
This distinction matters for struct constructors. Writing @implicit def __init__(out self, name: StringLiteral) lets structs accept bare string literals without callers writing String("...") — convenient, but see Implicit conversion in section 4 for when not to reach for it.

2 · Operators, Control Flow & Functions

Arithmetic operators · Exercise 03

print(7 + 3)     # 10 — addition
print(7 - 3)     # 4  — subtraction
print(7 * 3)     # 21 — multiplication
print(7 / 3)     # 2  — Int / Int stays an Int, truncated
print(7 // 3)    # 2  — floor division
print(7 % 3)     # 1  — modulo
print(2 ** 3)    # 8  — exponentiation

# / and // agree on positives and diverge on negatives:
print(-7 / 2)    # -3 — truncates toward zero
print(-7 // 2)   # -4 — floors toward negative infinity
Gotcha: / does not do true division. This is the sharpest break from Python. In Mojo the result type follows the operands, so Int / Int is an Int with the remainder discarded — no warning. It is also not a synonym for //: / truncates toward zero while // floors toward negative infinity, so they disagree the moment either operand is negative. For a real quotient, convert first:
print(Float64(7) / Float64(3)) # 2.3333...

// and % round toward −∞ (like Python, not C):

print(-7 // 3)    # -3
print(-7 % 3)     # 2

Comparison & membership operators

print(5 == 5)    # True
print(5 != 3)    # True
print(5 < 3)     # False
print(5 > 3)     # True

var text: String = "hello world"
print("world" in text)        # True  — substring
print("xyz" not in text)      # True

var xs = [1, 2, 3]            # Array[Int, 3] — see Collections
print(2 in xs)                # True  — element check
Gotcha: is / is not only work on types that implement __is__. Using is on an ordinary Int is a compile error. The idiomatic use is maybe is None for Optional.

Boolean & bitwise operators

and / or short-circuit and return the actual determining value, not a coerced Bool:

var zero = 0
var five = 5
print(zero or five)    # 5  (the value that determined the outcome)
print(6 & 3)      # 2  — AND
print(6 | 3)      # 7  — OR
print(6 ^ 3)      # 5  — XOR
print(~6)         # -7 — invert
print(1 << 3)     # 8  — left shift
print(16 >> 2)    # 4  — right shift

Control flow · Exercise 04

def grade(n: Int) -> String:
    if n >= 90:
        return "A"
    elif n >= 80:
        return "B"
    else:
        return "C"

print(grade(95))   # A
print(grade(85))   # B
print(grade(50))   # C

Ternary expression:

var x = 5
print("big" if x > 3 else "small")   # big

Loops:

var i = 0
while i < 3:
    i += 1
print("i =", i)        # i = 3

var total = 0
for k in range(5):
    total += k
print("total =", total)  # total = 10

Functions · Exercise 05

Functions are declared with def. Parameters can have defaults and can be supplied by name:

def add(a: Int, b: Int) -> Int:
    return a + b

def power(base: Int, exp: Int = 2) -> Int:
    var r = 1
    for _ in range(exp):
        r *= base
    return r

print(add(2, 3))           # 5
print(power(3))            # 9  (exp defaults to 2)
print(power(2, exp=5))     # 32 (keyword argument)

Functions that can fail: raises

A function that might fail must declare raises — the compiler enforces it:

def risky(x: Int) raises -> Int:
    if x < 0:
        raise Error("negative")
    return x

def main() raises:
    print(risky(10))    # 10
Gotcha: def main() is non-raising by default. If it calls a raises function, you must mark main() as raises too — otherwise the compiler refuses to compile it.

Position-only and keyword-only arguments

Mojo follows Python's / and * delimiter convention. Arguments before / must be positional; arguments after * must be named:

def pos_only(a: Int, b: Int, /) -> Int:    # a, b: positional only
    return a + b

def kw_only(*, x: Int, y: Int) -> Int:     # x, y: keyword only
    return x + y

def mixed(a: Int, /, b: Int, *, c: Int) -> Int:
    return a + b + c

print(pos_only(1, 2))             # 3
print(kw_only(x=10, y=20))        # 30
print(mixed(1, 2, c=3))           # 6
Calling pos_only(a=1, b=2) or kw_only(10, 20) are both compile errors — the delimiter is enforced by the compiler, not by convention.

Command-line arguments: argv()

Access command-line arguments via argv() from the standard library. Index 0 is the script name:

from std.sys import argv

def main():
    var args = argv()
    print("script:", args[0])
    if len(args) > 1:
        print("first arg:", args[1])
mojo run my_script.mojo hello
# script: my_script.mojo
# first arg: hello

3 · Strings, Collections & Errors

String operations · Exercise 06

var s: String = "  Hello Mojo  "

print(s.upper())              #   HELLO MOJO
print(s.lower())              #   hello mojo
print("[" + s.strip() + "]")  # [Hello Mojo]

var t: String = "hello world"
print(t.find("world"))        # 6 (byte offset; -1 if not found)
print(t.startswith("hello"))  # True
print(t.endswith("world"))    # True

var csv: String = "a,b,c"
var parts = csv.split(",")
print(parts)                  # [a, b, c]
print(String("-").join(parts))# a-b-c

String slicing gotcha

Plain numeric slicing is ambiguous for UTF-8 — Mojo requires you to say byte= or codepoint=:

var t: String = "hello world"
print(t[byte=0:5])          # hello
print(t[codepoint=6:11])    # world

# t[0:5]  ← compile error: use byte= or codepoint= explicitly
Gotcha: Pick the unit deliberately — byte= counts UTF-8 bytes, codepoint= counts Unicode code points. They diverge the moment a string contains a multi-byte character (é, emoji), so slicing by byte offset can split a character in half.

String interpolation: t"..." not f"..."

var name: String = "World"
print(t"Hello, {name}!")      # Hello, World!

var n = 42
var s: String = String(t"n={n}")   # must wrap explicitly — t"..." is TString, not String
print(s)                            # n=42
Gotcha: No Python f-strings. The t"" prefix produces a TString, not a String. Format the value before interpolating — format specs like {x:.2f} are rejected.

Collections · Exercise 07

List — typed, growable array. In Mojo 1.0, an unannotated list expression creates a fixed-size Array; annotate it when you need a List:

var xs = List[Int]()
xs.append(1); xs.append(2); xs.append(3)
print(len(xs))    # 3
print(xs[0])      # 1

var ys: List[Int] = [10, 20, 30]    # explicitly a List
var total = 0
for y in ys:
    total += y
print(total)      # 60

Dict — hash map:

var d = Dict[String, Int]()
d["a"] = 1
d["b"] = 2
print(d["a"])   # 1
print(len(d))   # 2

Optional — a value that may be absent:

var o = Optional[Int](5)
if o:
    print(o.value())   # 5

Tuple — fixed-length, mixed-type return value:

def minmax(a: Int, b: Int) -> Tuple[Int, Int]:
    return (a, b) if a < b else (b, a)

var lo, hi = minmax(9, 4)
print(lo, hi)   # 4 9

Set:

var a = Set[Int](1, 2, 3)
var b: Set[Int] = {2, 3, 4}
print(a | b)    # {1, 2, 3, 4} — union
print(a & b)    # {2, 3}       — intersection

Deque — double-ended queue, O(1) at both ends:

var dq = Deque[Int]()
dq.append(1); dq.append(2); dq.appendleft(0)
print(dq)           # [0, 1, 2]
print(dq.popleft()) # 0
print(dq.pop())     # 2

Counter — tally dictionary, missing keys return 0:

var counts = Counter[String]()
counts["a"] += 1; counts["a"] += 1; counts["b"] += 1
print(counts["a"])  # 2
print(counts["b"])  # 1

Array — fixed-size, stack-allocated:

var arr = Array[Int, 3](fill=0)
arr[0] = 1; arr[1] = 2; arr[2] = 3
print(arr[0], len(arr))   # 1  3

Variant — tagged union (no Union type by that name):

var v: Variant[Int, String] = 42
if v.isa[Int]():
    print(v[Int])            # 42

v = String("now a string")
if v.isa[String]():
    print(v[String])         # now a string

Comprehensions · Exercise 08

var squares = [n * n for n in range(5)]
print(squares)   # [0, 1, 4, 9, 16]

var evens = [n for n in range(10) if n % 2 == 0]
print(evens)     # [0, 2, 4, 6, 8]

var lookup = {n: n * n for n in range(4)}
print(lookup)    # {0: 0, 1: 1, 2: 4, 3: 9}

var uniq = {n % 3 for n in range(6)}
print(uniq)      # {0, 1, 2}
Gotcha: No generator expressions (x for x in ...) and no yield generator functions in this build. Comprehensions always build the full collection eagerly.

Error handling · Exercise 09

def checked_div(a: Int, b: Int) raises -> Int:
    if b == 0:
        raise Error("division by zero")
    return a // b

try:
    print(checked_div(10, 2))   # 5
    print(checked_div(1, 0))    # raises here
except e:
    print("caught:", e)          # caught: division by zero

else runs only when try succeeded; finally always runs:

try:
    print(checked_div(10, 5))
except e:
    print("caught:", e)
else:
    print("no exception occurred")  # 2  then this

try:
    raise Error("boom")
except e:
    print("caught:", e)
finally:
    print("cleanup always runs")    # always

Bare except: catches without binding; bare raise re-throws:

def outer() raises:
    try:
        inner()
    except:                    # catch-all — no variable
        print("logging then re-raising")
        raise                  # re-raise the caught error unchanged

4 · Structs, Traits & Closures

Mojo's struct vs C struct

Mojo's struct is closer to a C++/Rust struct than a C struct — it has fields, methods, constructors, and trait conformance. It is a value type: assignment copies, not references. There is no garbage collector involved.

A plain data struct · Exercise 10

@fieldwise_init generates a constructor from the var fields automatically:

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

var p = Point(1, 2)
print(p.x, p.y)   # 1  2

Methods use self for read-only access and mut self for mutating access — the compiler enforces the distinction:

@fieldwise_init
struct Counter:
    var n: Int

    def get(self) -> Int:      # read-only
        return self.n

    def inc(mut self):         # mutating
        self.n += 1

var c = Counter(0)
c.inc(); c.inc()
print(c.get())   # 2

Constructor overloading · Exercise 11

struct Point:
    var x: Int
    var y: Int

    def __init__(out self, x: Int, y: Int):
        self.x = x; self.y = y

    def __init__(out self, both: Int):    # overload
        self.x = both; self.y = both

    @staticmethod
    def origin() -> Point:               # named factory
        return Point(0, 0)

    def __init__(out self, x: Int = 0, y: Int = 0):  # defaults
        self.x = x; self.y = y
Don't ship this exact overload set. It is written out to show the shapes a constructor can take, not as a working struct — I compiled it, and 1.0.0 rejects it outright with "redefinition of function __init__ with identical signature". Defaults don't make a signature distinct: __init__(x: Int = 0, y: Int = 0) and __init__(x: Int, y: Int) are the same two-Int signature, so this fails where the struct is defined, not at any call site. Keep one of those two; the one-argument both: Int overload coexists with either.
Gotcha: @fieldwise_init vs Copyable/Movable/Writable. The decorator codegenerates __init__. Copyable, Movable, Writable are plain trait names — not keywords. struct is a keyword.

Implicit conversion: @implicit

Mark an __init__ with @implicit to allow automatic type coercion when the target type is needed. The compiler calls the constructor silently at the assignment site:

struct Vehicle:
    var model: String

    @implicit
    def __init__(out self, model: StringLiteral):
        self.model = model

var v: Vehicle = "Tesla"    # StringLiteral → Vehicle automatically
print(v.model)              # Tesla
Use sparingly. Implicit conversions shorten call sites but hide what is happening. Prefer explicit constructors for types where the conversion is non-obvious.

No classes, no struct inheritance · Exercise 12

Mojo has no class keyword. A struct cannot extend another struct. Both are hard compile errors, by design:

class Animal: ...        # error: classes are not supported yet

struct Dog(Animal):      # error: structs only conform to traits

Instead, use composition (has-a) and traits (can-do):

@fieldwise_init
struct Engine(Copyable, Movable):
    var horsepower: Int

    def describe(self) -> String:
        return String(self.horsepower) + " hp"

@fieldwise_init
struct Car(Copyable, Describable, Movable):
    var engine: Engine
    var name: String

    def describe(self) -> String:
        return self.name + " (" + self.engine.describe() + ")"

var e = Engine(300)
var c = Car(e^, "Tesla")
print(c.describe())   # Tesla (300 hp)

Traits & generics · Exercise 13

A trait is a contract — a list of method signatures a type promises to implement:

trait Describable:
    def describe(self) -> String: ...

@fieldwise_init
struct Robot(Describable):
    var id: Int

    def describe(self) -> String:
        return "unit-" + String(self.id)

def print_description[T: Describable](item: T):
    print(item.describe())

print_description(c)           # Tesla (300 hp)
print_description(Robot(7))    # unit-7

Traits compose — a combined trait requires all methods from its parts:

trait Shape(Drawable, Measurable):
    pass

@fieldwise_init
struct Square(Shape):
    var side: Float64
    def draw(self) -> String: return "[square]"
    def area(self) -> Float64: return self.side * self.side

Operator overloading

+ desugars to __add__. print() uses the Writable trait via write_to — and the struct must declare that conformance, not merely define the method. Mojo 1.0 dropped the implicit duck-typed conformance, so omitting (Writable) here fails with "an element of 'values' with type 'Vec2' does not conform to trait 'Writable'":

@fieldwise_init
struct Vec2(Writable):
    var x: Int
    var y: Int

    def __add__(self, other: Vec2) -> Vec2:
        return Vec2(self.x + other.x, self.y + other.y)

    def write_to[W: Writer](self, mut writer: W):
        writer.write("(", self.x, ", ", self.y, ")")

var a = Vec2(1, 2)
var b = Vec2(3, 4)
print(a + b)   # (4, 6)

Special methods: full dunder protocol

Mojo resolves operators and built-in functions to dunder method calls on the type. A complete reference of the most common ones:

Relational (comparison) operators — enable ==, !=, <, <=, >, >=:

@fieldwise_init
struct Temp:
    var celsius: Float64

    def __eq__(self, other: Temp) -> Bool:
        return self.celsius == other.celsius

    def __lt__(self, other: Temp) -> Bool:
        return self.celsius < other.celsius

    def __le__(self, other: Temp) -> Bool:
        return self.celsius <= other.celsius

var a = Temp(20.0); var b = Temp(30.0)
print(a == b)    # False
print(a < b)     # True

In-place operators (__iadd__, __isub__, __imul__, …) power +=, -=, *=:

@fieldwise_init
struct Counter2:
    var n: Int

    def __iadd__(mut self, step: Int):
        self.n += step

var c = Counter2(0)
c += 5; c += 3
print(c.n)    # 8

Reflected operators (__radd__, __rsub__, …) handle other + self when the left operand doesn't know how:

def __radd__(self, other: Int) -> Int:
    return other + self.n    # e.g. 10 + my_obj

Container protocol__getitem__, __setitem__, __contains__, __len__:

struct Bag:
    var items: List[String]

    def __init__(out self):
        self.items = List[String]()

    def __len__(self) -> Int:
        return len(self.items)

    def __getitem__(self, i: Int) -> String:
        return self.items[i]

    def __setitem__(mut self, i: Int, val: String):
        self.items[i] = val

    def __contains__(self, val: String) -> Bool:
        for item in self.items:
            if item == val:
                return True
        return False

var bag = Bag()
bag.items.append("apple")
print(len(bag))           # 1
print(bag[0])             # apple
print("apple" in bag)     # True

Conversion methods__bool__ enables truthiness checks; __int__ enables Int(obj):

@fieldwise_init
struct Score:
    var value: Int

    def __bool__(self) -> Bool:
        return self.value > 0

    def __int__(self) -> Int:
        return self.value

var s = Score(0)
if not s:
    print("zero score")    # zero score
print(Int(Score(42)))      # 42

Context manager protocol__enter__ / __exit__ power with statements:

@fieldwise_init
struct Timer:
    var label: String

    def __enter__(self) -> Self:
        print("start:", self.label)
        return self

    def __exit__(self):
        print("end:", self.label)

with Timer("batch"):
    print("doing work")
# start: batch
# doing work
# end: batch

Closures and lambdas · Exercise 14

Mojo 1.0 adds Python-style lambda expressions for short, single-expression closures. Named nested functions use an explicit capture list:

var n = 10

def add_n(x: Int) {imm n} -> Int:
    return x + n

var double = lambda (x: Int) -> Int: x * 2
print(add_n(5))   # 15
print(double(4))  # 8

Closures can mutate outer variables when the capture is marked mut:

var count = 0

def increment() {mut count}:
    count += 1

increment(); increment(); increment()
print(count)   # 3

Pass a closure to another function as a runtime argument. The function type is a compile-time parameter; the closure value is an ordinary argument:

def filter_list[
    P: def(Int) -> Bool
](xs: List[Int], pred: P) -> List[Int]:
    var result = List[Int]()
    for x in xs:
        if pred(x):
            result.append(x)
    return result^

var threshold = 5

def above_threshold(v: Int) {imm threshold} -> Bool:
    return v > threshold

var nums: List[Int] = [1, 5, 8, 2, 9, 3, 12]
var big = filter_list(nums, above_threshold)
# above 5: 8 9 12
Gotcha: Mojo does not yet support escaping closures — one cannot outlive the scope that created it. Passing a closure down into a function is fine; storing it in a struct or returning it from the enclosing function is not.

Lifecycle methods: __deinit__, copy, and move constructors

When a struct owns a resource (e.g. a Pointer), implement the lifecycle hooks to manage it correctly:

__deinit__ runs when the value goes out of scope or is explicitly destroyed. The deinit modifier tells the compiler the parameter is being consumed by this call. The old __del__ spelling still compiles but emits a deprecation warning:

from std.memory.alloc import unsafe_alloc

struct Buffer:
    var ptr: Pointer[Int, MutUntrackedOrigin]

    def __init__(out self, value: Int):
        self.ptr = unsafe_alloc[Int](1)
        self.ptr.unsafe_write(value)

    def get(self) -> Int:        # read through a method, not b.ptr[] — see below
        return self.ptr[]

    def __deinit__(deinit self):
        self.ptr.unsafe_deinit_pointee()
        self.ptr.unsafe_free()
Gotcha: an untracked origin will not keep the struct alive. Reading b.ptr[] directly from outside the struct is a use-after-free. Because MutUntrackedOrigin is, as the name says, untracked, the compiler can't see that the pointer depends on b — so ASAP destruction runs __deinit__ (freeing the memory) as soon as it has read the b.ptr field, before the dereference happens. I compiled this: print(b.ptr[]) printed 0, while the same read through b.get() printed the real value. Go through a method that borrows self, or keep the owner alive across the read with _ = b afterwards.

The copy constructor is spelled __init__(out self, *, copy: Self). Declare ImplicitlyCopyable on the struct to allow implicit copies:

struct Buffer(ImplicitlyCopyable):
    ...
    def __init__(out self, *, copy: Self):
        self.ptr = unsafe_alloc[Int](1)
        self.ptr.unsafe_write(copy.ptr[])   # deep copy

var b1 = Buffer(42)
var b2 = b1     # copy constructor called — b1 unchanged
Copyable vs ImplicitlyCopyable. These are two different traits, not two spellings of one. Copyable means the type can be copied explicitly, via copy() or the copy initializer — and it implies Movable. ImplicitlyCopyable is a marker trait layered on top: it grants the compiler permission to insert copies silently when a borrowed value is passed where an owned one is needed. Mojo 1.0 made explicit-only the default, so add ImplicitlyCopyable only when copying is cheap and side-effect free.

The move constructor is spelled __init__(out self, *, deinit move: Self). Declare Movable to allow ^ transfers:

struct Buffer(Movable):
    ...
    def __init__(out self, *, deinit move: Self):
        self.ptr = move.ptr    # steal the pointer — no allocation

var b1 = Buffer(99)
var b2 = b1^    # move constructor called — b1 no longer valid
Mojo uses eager destruction: a value is destroyed immediately after its last use, not at the end of the scope. To extend a value's lifetime to a specific point, assign it to _ there: _ = my_val.

5 · Ownership, Performance & Beyond

Copy vs transfer (^)

For an ImplicitlyCopyable type, plain assignment copies and the source stays valid:

var a: String = "hello"
var b = a           # independent copy — no ^
print(a)            # hello  (untouched)
print(b)            # hello

Adding ^ transfers ownership. The compiler statically tracks that the source is now invalid:

var a: String = "hello"
var b = a^          # a is now empty — compiler knows this

print(b)            # hello
print(a)            # error: use of uninitialized value 'a'
The check is entirely at compile time. ^ on a trivial type like Int is a no-op (the compiler warns but allows it). Note that the silent copy above works because String is ImplicitlyCopyable — that is a property of the type, not of assignment. List is Copyable but not implicitly so, so the same var b = a on a list fails with "not implicitly copyable because it doesn't conform to ImplicitlyCopyable". Write var b = a.copy() instead, or transfer with a^.

Function argument modifiers: read, var, mut, out

Every function parameter has an ownership modifier that controls what the callee can do with the value. imm is the default — it can be omitted:

# imm (default) — immutable borrow; caller's value untouched
def inspect(imm val: Int):        # same as just (val: Int)
    print(val)

# var — function receives its own copy; caller's value untouched
def consume(var val: Int):
    val += 100    # only modifies the local copy
    print(val)

# mut — mutable borrow; no copy; caller sees the change
def double_in_place(mut val: Int):
    val *= 2

# out — function initialises the reference; value is returned implicitly
def make_ten(out val: Int):
    val = 10

var x = 5
inspect(x)          # x = 5, x still 5
consume(x)          # prints 105, x still 5
double_in_place(x)  # x is now 10
print(make_ten())   # 10
Rule of thumb: use imm (or nothing) for pure reads, mut when you need to mutate without copying, var when the function legitimately owns a private copy, and out for constructor-style initialisation. The compiler enforces the contract at every call site. Mojo 1.0 renamed this convention from read to imm — the old spelling still compiles but warns "'read' is deprecated; use 'imm'", so older code and blog posts you find will use it.

File I/O · Exercise 15

from std.os import remove
from std.os.path import exists
from std.pathlib import Path

def main() raises:
    var path = "demo.txt"

    # write
    with open(path, "w") as f:
        f.write("hello file")

    # read
    with open(path, "r") as f:
        print(f.read())          # hello file

    # append
    with open(path, "a") as f:
        f.write(" — appended")

    # check existence
    print(exists(path))          # True
    print(Path(path).exists())   # True

    # handle missing file
    try:
        var _ = open("missing.txt", "r")
    except e:
        print("caught:", e)

    remove(path)
Prefer with open(...) as f: over manual open() / close() pairs — the context manager closes the file automatically even if an error occurs inside the block.

SIMD · Exercise 16

A SIMD value packs several numbers into one hardware register. A single operation applies to every lane at once — the low-level reason Mojo outperforms scalar Python:

var v = SIMD[DType.int32, 4](1, 2, 3, 4)

print(v)         # [1, 2, 3, 4]
print(v * 2)     # [2, 4, 6, 8]  — all four lanes multiplied simultaneously
print(v + v)     # [2, 4, 6, 8]

# Float lanes work too
var f = SIMD[DType.float64, 2](1.5, 2.5)
print(f * 2)     # [3.0, 5.0]
Int, Float64, and other scalar types are single-lane SIMD values under the hood. Writing kernels with multi-lane SIMD gives you CPU vectorisation without writing assembly.

Python interoperability · Exercise 17

Import any Python module with Python.import_module. The returned object wraps the real Python object:

from std.python import Python

def main() raises:
    var math = Python.import_module("math")
    var result = math.sqrt(2.0)
    print("Python math.sqrt(2.0) =", result)
    # Python math.sqrt(2.0) = 1.4142135623730951

The Python dependency is runtime-only, pay-per-use. The figures below are from my own machine — pixi-packaged Mojo 1.0.0 on x86-64 Linux — so expect different absolute numbers on a different toolchain or platform. The gap between them is the durable part:

  • 01_hello.mojo compiled binary: 18 KB, no libpython link
  • 17_python_interop.mojo compiled binary: 117 KB, dlopen()s CPython at runtime
Mojo never links libpython at build time. Interop dlopen()s a CPython runtime on demand, only if the program actually calls Python.import_module.

To run the exercise, CPython must be discoverable at runtime:

LIBPY=$(find .pixi/envs/default/lib -maxdepth 1 -name 'libpython3*.so' | head -1)
MOJO_PYTHON_LIBRARY="$LIBPY" pixi run mojo run exercises/17_python_interop.mojo

The other direction: calling Mojo from Python

Interop also runs the opposite way — build a Mojo module as a Python extension and import it from ordinary Python code. There is no exercise for this in the companion repo yet; the snippet below is the documented API, not something you can clone and run today:

# mojo_module.mojo
from std.python import PythonObject
from std.python.bindings import PythonModuleBuilder
from std import math
from std.os import abort

@export
def PyInit_mojo_module() abi("C") -> PythonObject:
    try:
        var m = PythonModuleBuilder("mojo_module")
        m.def_function[factorial]("factorial", docstring="Compute n!")
        return m.finalize()
    except e:
        abort(String("error creating Python Mojo module:", e))

def factorial(py_obj: PythonObject) raises -> PythonObject:
    # Raises an exception if `py_obj` is not convertible to a Mojo `Int`.
    var n = Int(py=py_obj)
    return math.factorial(n)

On the Python side, import mojo.importer installs an import hook that finds the matching .mojo source and compiles it on demand — via mojo build --emit shared-lib into a __mojocache__/ directory — rebuilding only when the source changes. Ship the .mojo file; the hook handles the build:

# main.py
import mojo.importer
import mojo_module

print(mojo_module.factorial(5))

python main.py prints 120. As an alternative to the import hook — useful when you need manual build flags — you can pre-build the module yourself with mojo build mojo_module.mojo --emit shared-lib -o mojo_module.so and distribute the resulting .so directly.

Two things to know before reaching for this. PyInit_<module>() needs both @export and abi("C") — functions registered through def_function (like factorial above) don't need @export themselves. And the binding layer currently caps out at 6 PythonObject arguments per function.

6 · Advanced Topics

Topics in this section are sourced from ruhati.net/mojo ("Mojo By Example" by Indukumar Vellapillil-Hari) and adapted from Session 6 of the training deck. They map to chapters 6–21 of the standard Mojo course outline. Treat code snippets as starting points — verify against your installed Mojo version before relying on them.

Documenting your code

A triple-quoted string placed as the first statement inside a function is a docstring. mojo doc generates reference documentation from it:

def my_function(text: String):
    """
    This is a docstring summary.
    A second line can add more detail.
    """
    print(text)

# generate docs:
# pixi run mojo doc my_file.mojo

Parameters & compile-time programming

Square brackets always mean "resolve this at compile time". Parentheses always mean "supply at runtime". A parameter can be a type, a value, or even a Bool:

def add[cond: Bool](a: Int, b: Int) -> Int:
    return a + b if cond else 0

print(add[True](3, 4))    # 7  — cond is baked in at compile time

struct MyStruct[T: Intable]:
    var value: Int

    def __init__(out self, value: Self.T):    # Self.T, not bare T
        self.value = Int(value)

Ownership & lifecycle

The four argument modifiers imm / var / mut / out (covered in S5) give you control over borrowing and ownership at every call site. The transfer operator ^ moves ownership into a function declared with a move constructor:

def take_owned(var b: String):    # receives its own copy
    print(b)

def borrow_only(b: String):       # read (default) — no copy
    print(b)

var s = "hello"
borrow_only(s)     # s still valid
take_owned(s^)     # s transferred — no longer valid after this

Origin tracking and ref[o] references

Mojo's compiler tracks the origin of every value — the variable that originally owns it. You can express this explicitly with ref[o] to write functions that return a reference bound to the lifetime of an argument:

def my_function_modify[o: MutOrigin](ref[o] a: MyStruct):
    a.set_value(20)     # mutates caller's value in place

struct Container:
    var item: Widget

    def get_item(self) -> Pointer[Widget, origin_of(self.item)]:
        return Pointer(to=self.item)  # pointer lifetime tied to self
ImmutOrigin and MutOrigin are the two origin kinds. origin_of(expr) extracts the origin of an existing value. This is an advanced feature needed when building container types and when returning interior references — most application code never needs it directly.

Linear types

A linear type must be explicitly destroyed — the compiler errors if you drop it silently. Opt out of Deinitable and annotate the struct with @explicit_destroy:

@explicit_destroy("Must call destroy_me() explicitly")
@fieldwise_init
struct Linear(Deinitable where False):
    var value: Int

    def destroy_me(deinit self):
        print("Destroying Linear with value", self.value)

var l: Linear = Linear(10)
l^.destroy_me()    # explicit destruction required — dropping l silently is a compile error
Linear types are useful for resources that must not be leaked — file handles, GPU command buffers, network connections. The compiler becomes your resource-leak detector. The trait was called ImplicitlyDestructible before 1.0 and briefly ImplicitlyDeletable; both still resolve as deprecated aliases, so older code you find in the wild may spell it either way.

Memory management, references & pointers

For manual memory control, Mojo provides a unified Pointer type. Operations that need an unsafe escape hatch are marked explicitly:

from std.memory.alloc import unsafe_alloc

var p = unsafe_alloc[Int](1)
p.unsafe_write(copy=10)
print(p[])            # 10
p.unsafe_deinit_pointee()
p.unsafe_free()
UnsafePointer still resolves as a deprecated alias for Pointer (it warns). Note that origin is a required parameter — the full signature is Pointer[T, origin], and only address_space is defaulted. It is inferred when you write unsafe_alloc[Int](1), but a written-out type annotation must supply it. Use unsafe_alloc rather than a bare alloc: plain alloc[Int](1) now warns "alloc without a Layout is deprecated" and points you at either unsafe_alloc or the layout-aware alloc[Int]({count = 1}), which returns an explicitly-destroyed Allocation released with dealloc(allocation^). The allocation is uninitialised — reading before you write gives garbage, not zero.

Metaprogramming

Mojo can branch on trait conformance at compile time, producing different code per type with zero runtime cost:

def multiply(first: Scalar, second: type_of(first)) -> type_of(first):
    return first * second

def ability[T: Animal](x: T):
    if conforms_to[T, CanFly]():    # resolved at compile time
        print("it can fly")

Advanced usage of functions

Variadic arguments and overload resolution by argument type:

def add_all(*args: Int) -> Int:
    var total = 0
    for a in args:
        total += a
    return total

print(add_all(1, 2, 3, 4))    # 10

def add(a: Int, b: Int) -> Int: return a + b
def add(a: Int, b: Bool) -> Int: return a + 2 if b else a  # overload
Overloads that differ only in return type are rejected. Mojo resolves overloads strictly by argument and parameter types.

Modules & packages

A module is a .mojo file. A package is a directory with an __init__.mojo:

# my_module.mojo
def print_module_message():
    print("hello from my_module")

# main.mojo
import my_module
my_module.print_module_message()

# distribute as a compiled package:
# pixi run mojo package mypackage

C interoperability

Call C functions directly via external_call, load shared libraries, or export Mojo functions for C to call:

from std.ffi import c_int, external_call

_ = external_call["rand", c_int]()    # calls libc's rand()

@export
def add(a: Int32, b: Int32) abi("C") -> Int32:    # callable from C
    return a + b
# mojo build --emit object add.mojo
@export needs an explicit abi("C") effect — without it the compiler warns "@export requires an explicit 'abi()' effect on the function". This is the same requirement PyInit_<module>() has in the Python-bindings section above. Note also the _ =: discarding a return value you don't use silences "assignment to 'r' was never used".

MLIR interoperability

Mojo exposes its compiler's own IR for systems-level work below any higher-level abstraction:

var bits: __mlir_type.i1

var idx = __mlir_op.`index.castu`[_type=__mlir_type.index](value)
This is the escape hatch beneath the whole language — for hardware-specific work that Mojo's normal types don't expose. Rarely needed in application code.

GPU programming

The same language used for hello-world can write GPU kernels — no CUDA or separate shading language required. As of Mojo 1.0 the accelerator APIs live in the max package rather than the standard library, so they are imported from max.gpu:

from max.gpu import block_dim, block_idx, thread_idx
from max.gpu.host import DeviceContext

def add_two(
    result: Pointer[Float32, MutUntrackedOrigin],
    a: Pointer[Float32, MutUntrackedOrigin],
    b: Pointer[Float32, MutUntrackedOrigin]
):
    var i = block_idx.x * block_dim.x + thread_idx.x
    result[i] = a[i] + b[i]

with DeviceContext() as ctx:
    var compiled = ctx.compile_function[add_two]()
    ctx.enqueue_function(compiled, result_buf, a_buf, b_buf,
                         grid_dim=8, block_dim=8)
    ctx.synchronize()
thread_idx / block_idx identify which of the thousands of parallel GPU thread invocations is currently running — the same kernel body executes once per GPU thread.

Further reading