IMPORTANT: To view this page as Markdown, append `.md` to the URL (e.g. /docs/manual/basics.md). For the complete Mojo documentation index, see llms.txt.
Skip to main content
Version: 1.2.0
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 function declarations reference

A function declaration introduces a named, callable unit of code. Every function in Mojo starts with the def keyword:

def greet(name: String) -> String:
return "Hello, " + name

The simplest function has a name, empty parentheses, and a body:

def do_nothing():
pass

Function names​

Function names must be valid identifiers. Backtick-escaped identifiers allow keywords as function names:

def `import`():
print("In `import`")

def main():
`import`() # In `import`

Function signatures​

def name(argument-list) -> ReturnType:
body

def name[parameter-list](argument-list) -> ReturnType:
body

def name(argument-list) raises -> ReturnType:
body

def name[parameter-list](argument-list)
-> ReturnType where constraint:
body

def name[parameter-list](argument-list) raises
-> ReturnType where constraint:
body

A signature can include a name, a parameter list, an argument list, effects, a return type, and a where clause. Only the parentheses and the colon are required.

Arguments are runtime values in parentheses. Parameters are compile-time values in square brackets. In other languages these are both called "parameters". Mojo distinguishes them to avoid confusion:

# T must be both `Comparable` (to test with `<` and `>`) and
# `ImplicitlyCopyable` or you won't be able to return
def clamp[T: Comparable & ImplicitlyCopyable](
val: T, lo: T, hi: T,
) -> T:
if val < lo:
return lo
if val > hi:
return hi
return val

Markers​

Three markers divide parameter and argument lists into zones that control how callers pass values:

MarkerArgumentsParameters
//NoInfer-only
/Positional-onlyPositional-only
*Keyword-onlyKeyword-only

Markers must appear in this order: //, then /, then *. Each can appear once. / can't be first in the list, and * can't be last.

Infer-only marker (//, parameters only)​

// separates infer-only parameters from named parameters. The compiler deduces infer-only parameters from call-site arguments:

def inferred_type[T: Writable, //](value: T):
print(t"Value is {value}. Type is {reflect[T].name()}.")

def main():
inferred_type(5) # Value is 5. Type is SIMD[DType.int, 1].
inferred_type("Hello") # Value is Hello. Type is String.

Infer-only parameters can't be specified positionally:

# Error because 'inferred_type' got 1 positional parameter
# but expected none.
inferred_type[Int](5)

Keyword syntax bypasses this restriction:

inferred_type[T=Int](5) # OK: Value is 5. Type is Int.

# Error because value passed to 'value' cannot be converted from
# 'StringLiteral["Hello"]' to 'Int'
inferred_type[T=Int]("Hello")

Inference isn't limited to infer-only parameters. With enough context, the compiler can infer named parameters too:

def add[T: Intable](a: T, b: T) -> Int:
return Int(a) + Int(b)

def main():
print(t"Sum is {add[Int](1, 2)}.") # Explicit T
print(t"Sum is {add(1, 2)}.") # Inferred T
print(t"Sum is {add[Float64](4.5, 1.2)}.") # Explicit
print(t"Sum is {add(4.5, 1.2)}.") # Inferred

Positional-only marker (/)​

Everything before / is positional-only. Callers must pass these values by position, not by name:

def div(a: Int, b: Int, /):
return a // b

div(10, 3) # OK
div(a=10, b=3) # Error

Keyword-only marker (*)​

Everything after * is keyword-only. Callers must pass values by name, whether parameters or arguments:

def configure(*, verbose: Bool, retries: Int):
# ...

configure(verbose=True, retries=3) # OK
configure(True, 3) # Error

A *args variadic argument has the same effect on arguments that follow it:

def sum(*values: Int, name: String) -> Int:
print(name, end=": ")
var total = 0
for value in values:
total += value
return total

def main():
print(sum(1, 2, 3, name="total")) # total: 6
# print(sum(1, 2, 3, "subtotal"))
# Error because missing required keyword argument

Default values​

Arguments can have default values. Once a default appears, every following positional argument must also have one:

def connect(
host: String = "www.modular.com", port: Int = 80,
):
print(t"Connecting to {host}:{port}")

def main():
connect() # Connecting to www.modular.com:80
connect(port=8080) # Connecting to www.modular.com:8080
def my_function(x: Int, y: Int = 0, z: Int = 0) -> Int:
return x + y + z

# Error because required positional argument follows optional
# positional argument
# def wrong(x: Int, y: Int = 0, z: Int):
# return x + y + z

Keyword-only arguments are exempt from the ordering rule. They can mix required and optional freely:

def configure(*, retries: Int = 3, verbose: Bool):
pass

Parameters also support defaults.

Function constraints​

A where clause constrains compile-time parameters. It appears at the end of the declaration, after the return type (or after the argument list if there's no return type):

comptime LESS_THAN: Int32 = -1
comptime EQUAL: Int32 = 0
comptime GREATER_THAN: Int32 = 1

def compare[T: AnyType](
x: T, y: T,
) -> Int32 where conforms_to(T, Comparable):
if x < y:
return LESS_THAN
elif x > y:
return GREATER_THAN
else:
return EQUAL

def main():
print(compare(5, 10)) # -1 (LESS_THAN)
print(compare(7, 7)) # 0 (EQUAL)
print(compare("Z", "A")) # 1 (GREATER_THAN)

where clauses can express complex constraints, such as limiting SIMD vector sizes to certain powers of 2:

def process[
n: Int,
](data: SIMD[.float32, n]) -> Float32 where (
n == 1 or n == 2 or n == 4 or n == 8 or n == 16 or n == 32
):
var sum: Float32 = 0.0
for i in range(n):
sum += data[i]
return sum


def main():
var data = SIMD[.float32, 16](255.0)
var sum = process[n=16](data)
print(t"Sum: {sum}") # Sum: 4080.0

where clauses belong at the end of a declaration:

# Correct: the `where` clause follows the signature.
def correct[n: Int]() where n > 0:
pass

A where clause inside a parameter list is invalid. Add it to the end of the declaration:

# Wrong: `where` is not allowed inside a parameter list.
def wrong[n: Int where n > 0]():
pass

A where clause in an argument list is invalid:

# Wrong: `where` clauses can only be used with compile-time parameters.
def wrong(x: Int where x > 0):
pass

A thin function type can carry its own trailing where clauses, constraining the parameters that type declares. See Constrained function types.

Argument conventions​

An argument convention controls how an argument value passes to a function. It appears before the argument name.

mut​

The caller's value is passed by mutable reference. Changes inside the function are visible to the caller:

def double_it(mut x: Int):
x *= 2

mut arguments can't have default values:

# Error because 'mut' arguments may not have defaults
def wrong(mut x: Int = 0):
pass

var​

The function receives an owned copy. If the caller transfers ownership with ^, the original becomes inaccessible. Otherwise the value is copied and the caller keeps access:

def consume(var s: String):
s += "!"
print(s)

def main():
var greeting = "Hello"
consume(greeting) # Hello! (copied)
print(greeting) # Hello

consume(greeting^) # Hello!
# print(greeting) # Error because uninitialized after transfer

out​

An out argument is the function's return slot. Only one out argument is allowed. It replaces the -> return type:

def make_int(out result: Int):
result = 42

def main():
var x = make_int()
print(x) # 42

A function can't use both out and -> Type:

# Error because function cannot have both an 'out' argument
# and an explicit result type
def wrong(out result: Int) -> Int:
result = 0

deinit​

The function takes ownership and destroys the value. Required for self in __deinit__() and the argument in move initializers:

struct Resource:
var handle: Int

def __deinit__(deinit self):
_release(self.handle)

ref​

Passes a reference with an explicit origin specifier. The origin tracks where the reference came from:

def get_first[T: Copyable](ref data: List[T]) -> ref[data[0]] T:
return data[0]

def main():
var data: List[String] = ["one", "two", "three"]
ref first = get_first(data) # mutable because `data` is mutable
print(first) # one
first = "Первый"
print(data) # ['Первый', 'two', 'three']

Default convention​

Without a convention, the argument is an immutable read-only reference. The caller keeps ownership:

def length[T: Copyable](s: List[T]) -> Int:
return len(s)

Variadic arguments​

Variadic arguments accept a varying number of values ("indefinite arity"). Functions like print use variadic arguments to accept any number of values.

Homogeneous variadics​

* before the argument name accepts any number of positional arguments of the same type (homogeneous arguments):

def sum_all(*values: Int) -> Int:
var total = 0
for v in values:
total += v
return total

Variadic packs​

* before both the name and the type annotation creates a variadic pack that accepts arguments of different types (heterogeneous arguments):

def print_all[*Ts: Writable](*args: *Ts):
comptime for idx in range(args.__len__()):
print(args[idx], end=" ")
print()

def main():
print_all("Hello", 42, 3.14) # Hello 42 3.14

Variadic restrictions​

A function can have at most one *args. Variadic arguments can't have default values:

# Error because variadic arguments may not have defaults
def wrong(*args: Int = 0):
pass

out arguments can't be variadic:

def wrong(out *results: Int):
pass

Function effects​

Effects appear after the closing parenthesis and before ->.

raises​

Declares that the function can raise an error. An optional error type can follow raises:

def parse(text: String) raises -> Int:
# ...

def parse_strict(text: String) raises SomeError -> Int:
# ...

A function can specify at most one error type after raises.

thin​

Used only in function types, thin indicates a function pointer type (not a closure) and ensures the function value doesn't capture values from its defining scope. Don't use thin in function declarations.

def map[
T: Copyable, U: Copyable
](f: def(T) thin -> U, input: List[T]) -> List[U]: # Used as type
var result: List[U] = []
for item in input:
result.append(f(item))
return result^

# `square` doesn't capture values. It can be passed to a thin type
def square(x: Int) -> Int:
return x * x

def main():
var nums: List[Int] = [1, 2, 3]
var squares = map(square, nums)
print(squares) # Output: [1, 4, 9]

Constrained function types​

A thin function type that declares parameters can constrain them with trailing where clauses, written after the result type. The constraint is part of the type: callers must prove it holds for the parameters they bind.

comptime Kernel = def[w: Int](Int) thin -> None where (
w > 0, "width must be positive"
)

def apply[F: Kernel](x: Int):
F[4](x) # ok
F[0](x) # error: violated constraint

Constraints are contravariant. A function satisfies the type when its own where clause is implied by the type's, or when it has none — the type promises its callers more than the function demands. A function that demands more than the type promises is rejected.

Only thin function types accept a where clause. The clause binds to the innermost function type, so a declaration-level where that follows a function-type result needs that result parenthesized:

# The `where` clause constrains the returned function type.
def inner_constraint[n: Int]() -> def() thin -> None where n > 0: ...

# The `where` clause constrains `outer_constraint` itself.
def outer_constraint[n: Int]() -> (def() thin -> None) where n > 0: ...

abi("C")​

Declares that a function uses the C calling convention. Because C has no closure mechanism, abi("C") normally appears together with thin in function types:

# `add` is compiled with the C calling convention, so it can be called
# from C or stored in a C-ABI function pointer.
def add(a: Int32, b: Int32) abi("C") -> Int32:
return a + b

def main():
var fp: def(Int32, Int32) thin abi("C") -> Int32 = add
print(fp(1, 2)) # 3

Return type​

-> introduces the return type. It appears after any effects:

def square(x: Int) -> Int:
return x * x

Without ->, the function returns None.

Special methods​

Certain method names have enforced signatures. The compiler checks argument count, conventions, and return types.

Initializers​

An initializer must have an out self result:

struct Point:
var x: Int
var y: Int

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

Without out self, the compiler rejects the method:

# Error because __init__ method must return Self type
# with 'out' argument
def __init__(self):
pass

Copy initializers​

A copy initializer uses a single keyword-only argument named copy:

def __init__(out self, *, copy: Self):
self.x = copy.x
self.y = copy.y

Copy a value with Type.__init__(copy=value), Type(copy=value), or value.copy():

  • The copy argument must use the default convention of a readable immutable reference.
  • Copy initializers can't raise.
  • Trivial types can't define a copy initializer.
  • Conforming to Copyable or ImplicitlyCopyable automatically generates a copy initializer if one isn't already defined.
  • If the type isn't compatible with copying, the compiler rejects the conformance.

Don't confuse copying with casting. Type(value) casts in Mojo. Type(copy=value) and value.copy() copy.

Prefer the explicit .copy() call over the copy keyword argument form for idiomatic Mojo style.

Move initializers​

A move initializer uses a single keyword-only argument named move:

def __init__(out self, *, deinit move: Self):
self.x = move.x
self.y = move.y

Call Type.__init__(move=value) or Type(move=value):

  • The move argument must use the deinit convention.
  • Move initializers can't raise.
  • RegisterPassable types can't define a move initializer. They're always movable by copying a register.

Deinitializers​

A deinitializer takes deinit self:

def __deinit__(deinit self):
_release(self.handle)
  • Deinitializers can't raise.
  • Trivial types can't define a deinitializer.

You can call value.__deinit__() explicitly but the compiler calls it automatically at the value's last use, via ASAP destruction. Prefer custom deinitializers for explicit destruction.

Nested functions​

Functions can be defined inside other functions. Nested functions can capture values from the enclosing scope as closures:

def outer(x: Int) -> Int:
def inner() {imm} -> Int: # Read-only references (`imm`) to outer scope
return x + 1
return inner()

The compiler resolves nested function bodies immediately so captures bind correctly.

Static methods​

@staticmethod makes a struct method callable without an instance. Static methods don't take self:

struct MathUtils:
comptime pi: Float64 = 3.141592653589793

@staticmethod
def square(x: Int) -> Int:
return x * x

def main():
print(MathUtils.square(5)) # 25
print(MathUtils.pi) # 3.141592653589793