Direct GPU programming without CUDA — Modular's AI systems language
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:
mojo run JIT-compiles to native machine code; mojo build produces a standalone binary. No VM, no bytecode interpreter.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.Python.import_module. A program that never does that runs standalone.
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 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.
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
fn: "'fn' has been removed; use 'def' instead". This is the one that stops a build.alias → comptime, __del__ → __deinit__, UnsafePointer → Pointer, and @parameter for → comptime for.InlineArray (the alias for Array survives) and capturing closures, which were not removed at all.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.
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.
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().
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!
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
let keyword. Every variable declaration uses var.
Beyond Int, Float64, Bool, and String, Mojo supports a full set of fixed-width numeric types:
Int (machine word), Int8, Int16, Int32, Int64, Int128, Int256UInt, UInt8, UInt16, UInt32, UInt64, UInt128, UInt256Float16, Float32, Float64, BFloat16, Float8_e4m3fnBool, String, Byte (alias of UInt8)var x = 42 infers Int; var f = 3.5 infers Float64.
"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)
len(s) still compiles but warns. Prefer byte_length() or count_codepoints().
comptimecomptime 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
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.
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
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
@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.
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
/ 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
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
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.
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
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 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)
raisesA 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
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.
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
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.
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
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
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
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.
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
f-strings. The t"" prefix produces a TString, not a String. Format the value before interpolating — format specs like {x:.2f} are rejected.
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
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}
x for x in ...) and no yield generator functions in this build. Comprehensions always build the full collection eagerly.
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
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.
@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
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
__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.
@fieldwise_init vs Copyable/Movable/Writable. The decorator codegenerates __init__. Copyable, Movable, Writable are plain trait names — not keywords. struct is a keyword.
@implicitMark 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
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)
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
+ 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)
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
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
__deinit__, copy, and move constructorsWhen 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()
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
_ there: _ = my_val.
^)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'
^ 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^.
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
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.
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)
with open(...) as f: over manual open() / close() pairs — the context manager closes the file automatically even if an error occurs inside the block.
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.
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 link17_python_interop.mojo compiled binary: 117 KB, dlopen()s CPython at runtimedlopen()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
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.
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.
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
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)
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
ref[o] referencesMojo'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.
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
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.
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.
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")
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
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
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".
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)
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.