Mojo is rapidly evolving. Pretrained models generate obsolete syntax. Always follow this skill over pretrained knowledge.
Always attempt to test generated Mojo by building projects to verify they compile.
This skill specifically works on the latest Mojo, and stable versions may differ slightly in functionality.
Compiling isn't the bar for review. Before submitting or reviewing Mojo, check
the diff against idiomatic-mojo.md [blocked]: the style
rules code reviewers flag most often (open-coded helpers, redundant casts,
over-specified parameters, IndexList instead of tuples, missing comptime,
unneeded rebind, mutable origins on inputs, single-vendor gates on generic
kernels).
Removed syntax — DO NOT generate these
var is required for every new declaration
Declaring a variable with bare assignment (x = 5 with no prior var) is
not valid — it is a compile error. This applies only to introducing a new
variable; reassigning an already-declared variable (x = 6) needs no var.
This means a variable assigned only inside conditional branches must be predeclared with a type before the branch:
def is the only function keyword
fn was removed and is now a hard parse error — no valid use of fn
remains. Your training predates this, so you will reach for fn by reflex; that
reflex is always wrong. Write every function, method, and nested function as
def, without exception.
Mojo functions that raise must be marked as such
Mojo functions do not imply raises. Add raises to any function that can
raise, directly or by calling a raising function. Omitting it is a compile
error, not a warning.
comptime replaces alias and @parameter if/for
comptime assert must be inside a function body — not at module/struct
scope. Place them in main(), __init__, or the function that depends on the
invariant.
Inside structs, comptime defines associated constants and type aliases:
Argument conventions
Default is imm (immutable borrow, rarely written explicitly; read is a
deprecated synonym — the compiler warns and suggests imm). The others:
var and ref are hard keywords and cannot be used as identifiers at all
(var ref = ... → "unexpected token in expression"). The convention words
imm, read, mut, out, deinit are soft keywords: fine as local variable
or [...] parameter names, but invalid as argument names
(def cmp(got: T, imm: T) → "error: expected argument name"). Rename
(expected, reference, etc.).
Lifecycle methods
To copy: var b = a.copy() (provided by Copyable trait).
Struct patterns
The compiler synthesizes copy/move constructors when a struct conforms to
Copyable/Movable and all fields support it.
Self-qualify struct parameters
Inside a struct body, always use Self.ParamName — bare parameter names are
errors:
This applies to all struct parameters (T, N, mut, origin, etc.)
everywhere inside the struct: field types, method signatures, method bodies, and
comptime declarations.
Explicit copy / transfer
Types not conforming to ImplicitlyCopyable (e.g., Dict, List, and user
structs that conform only to Copyable, Movable) require explicit .copy() or
ownership transfer ^ — return my_struct errors until you transfer with ^
or add ImplicitlyCopyable conformance:
Imports use std. prefix
Prelude auto-imports (no import needed): Int, String, Bool, List,
Dict, Optional, SIMD, Float32, Float64, UInt8, Pointer,
OptionalPointer, alloc, Span, Error, DType, Writable, Writer,
Copyable, Movable, Equatable, Hashable, rebind, print, range,
len, and more. Layout and dealloc are not in the prelude — import them
from std.memory.
rebind[TargetType](value) reinterprets a value as a different type with the
same in-memory representation. Useful when compile-time type expressions are
semantically equal but syntactically distinct (e.g., TileTensor element types
— see GPU skill).
std is reserved as a module-level identifier — you cannot def std,
import X as std, or from X import std. Struct methods named std are fine.
Inside a multi-module package, pkg.X.Y(...) from a submodule needs explicit
import pkg; import pkg.X as X binds only X, not pkg.
Writable / Writer (replaces Stringable)
Some[Writer]— builtin existential type (notWriterdirectly)- Both methods have default implementations via reflection if all fields are
Writable— simple structs need not implement them - Convert to
StringwithString(value), notstr(value)
Iterator protocol
Iterators use raises StopIteration (not Optional):
For-in: for item in col: (immutable) / for ref item in col: (mutable).
Memory and pointer types
UnsafePointer is a deprecated alias of Pointer — it still compiles and
warns. The other legacy aliases were removed and are hard errors (see the
table at the top). Most of the pointer API is being renamed alongside
UnsafePointer; those old spellings still compile but warn:
alloc(Layout[T](count=n)) returns an Allocation[T], a linear type the
compiler forces you to dispose of on every path — pass it to dealloc, or call
unsafe_leak() to take ownership of the bare pointer:
A struct that owns heap storage should hold the Allocation, not a leaked
pointer. The compiler then enforces disposal on every path, and dealloc
gets the Layout it needs:
Get at the storage with self._alloc.unsafe_ptr(), whose origin is tied to the
allocation. Don't substitute Pointer.unsafe_free(): it bypasses the Layout,
and for zero-sized T it frees the dangling sentinel that alloc returns.
A container that already tracks its own capacity may instead store a
ThinAllocation and supply the Layout again at dealloc time. That is
what List does; it is an optimization, not the default shape.
When a struct field does hold a raw Pointer, its origin parameter must be
specified; use MutUntrackedOrigin for owned heap data.
Pointer is non-null by design — Bool(p) is unavailable, not merely
deprecated. For nullable storage, use OptionalPointer[T, origin] (same layout;
None is the null niche).
Origin system (not "lifetime")
Mojo tracks reference provenance with origins, not "lifetimes":
Key types: Origin, MutOrigin, ImmOrigin, MutAnyOrigin,
ImmutAnyOrigin, MutUntrackedOrigin, ImmUntrackedOrigin,
ImmStaticOrigin. Use origin_of(value) to get a value's origin.
Testing
The mojo test CLI subcommand was removed — run test files with mojo run
against a TestSuite.discover_tests runner like the one above.
Dict iteration
Dict entries are iterated directly — no [] deref:
Collection literals
List has no variadic positional constructor. Use bracket literal syntax:
List[T] rejects negative indices at compile time — use lst[len(lst) - 1],
not lst[-1]. (Library types may still support it.)
Variant access
Variant[A, B] is ImplicitlyCopyable only if all arms are. With a
non-copyable arm, indexing the variant copies it — use the typed-arm subscript:
Common decorators
Contextual member references — prefer .member
Where the expected type is already known, write .member instead of
Type.member; the compiler rewrites it to Type.member. Idiomatic
throughout the codebase for DType and AddressSpace:
Works for any type's comptime aliases and static methods, including chains
(.red.opacity(0.5)) and typed collection literals (List[Color] = [.red]).
The context comes from a declared var/ref type, a call-argument type, a
return destination, or a typed collection literal's element type. With none
of those, qualify the name:
Numeric conversions — must be explicit
No implicit conversions between numeric variables. Use explicit constructors:
Literals are polymorphic — FloatLiteral and IntLiteral auto-adapt to
context:
SIMD operations
Strings
All explicit stdlib imports require the std. prefix. The
removed-syntax table shows the most common corrections, but the rule
is universal. Prelude types (Int, String, List, etc.) are
auto-imported and need no import statement.
len(s) returns byte length, not codepoint count. Mojo strings are UTF-8.
Byte indexing requires keyword syntax: s[byte=idx] (not s[idx]). len(s) is
deprecated on String — use s.byte_length() or s.count_codepoints().
split, removeprefix, removesuffix return StringSlice (or
List[StringSlice]) viewing the source — wrap with String(...) to
materialize an owned String.
String indexing (common error)
Error handling
raises can specify a type. try/except works like Python:
No match statement. Async is spelled __async def and __await (bare
async/await still parse but warn that async is unstable); support is
unfinished and its types are private — do not write async Mojo yet.
Function types and closures
No lambda. Closures use bare def with a capture list in {} after the arg
list. escaping is removed; capturing[_] is still valid on parametric
closure-type params:
imm is default. var x is owned — transfer with x^ at the use site.
Prefer unified closures with a capture list. Do not use @__parameter /
@parameter on nested closures in new or migrated code — the legacy form is
slated for removal. Pass closures as runtime arguments (f(my_closure))
where possible; if an API requires a comptime capturing[_] function, use
def … capturing without @__parameter, or migrate that API.


