Skip to main content
Version: 1.0

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) 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:

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
//NoInferred
/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 inferred parameters from named parameters. The compiler deduces inferred parameters from call-site arguments:

from std.reflection import get_type_name

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

def main():
inferred_type(5) # Value is 5. Type is Int.
inferred_type("Hello") # Value is Hello. Type is String.

Infer-only parameters can't be specified positionally:

# Error: 'inferred_type' expects 0 positional parameters,
# but 1 was specified
inferred_type[Int](5)

Keyword syntax bypasses this restriction:

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

# Error: 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:

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=": ")
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: 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: required positional argument follows optional
# positional argument
# def bad(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 after the return type (or after the argument list if there's no return type):

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

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

where can also appear at the end of the full declaration:

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

def compare[T: AnyType](
a: T, b: T,
) -> Int32 where conforms_to(T, Comparable):
var x = trait_downcast[
Comparable & ImplicitlyCopyable
](a)
var y = trait_downcast[
Comparable & ImplicitlyCopyable
](b)
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 aren't valid on runtime arguments:

# Error: where clauses can only be used for compile time parameters
def bad(x: Int where x > 0):
pass

Argument conventions

An argument convention controls how a 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: 'mut' arguments may not have defaults
def bad(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! (moved)
# print(greeting) # Error: uninitialized after move

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: function cannot have both an 'out' argument
# and an explicit result type
def bad(out result: Int) -> Int:
result = 0

out arguments can't have defaults and can't be variadic.

deinit

The function takes ownership and destroys the value. Required for self in __del__ and the existing argument in move constructors:

struct Resource:
var handle: Int

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

Destructors can't raise. Trivial types can't define a destructor.

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 [origin_of(data)]T:
return data[0]

def main():
data = ["one", "two", "three"]
first = get_first(data)
print(first) # one

Default convention

Without a convention, the argument is immutable and borrowed. 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"). This is seen in functions like print that can take any number of arguments.

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: variadic arguments may not have defaults
def bad(*args: Int = 0):
pass

out arguments can't be variadic:

# Error: 'out' convention may not be variadic
def bad(out *results: Int):
pass

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 ValueError -> Int:
...

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

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.

__init__

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: __init__ method must return Self type
# with 'out' argument
def __init__(self):
pass

Copy constructor

A copy constructor is an __init__ with a single keyword-only argument named copy:

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

The copy argument must use the read convention (the default). Copy constructors can't raise. Trivial types can't define a copy constructor.

Move constructor

A move constructor is an __init__ with a single keyword-only argument named take:

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

The take argument must use the deinit convention. Move constructors can't raise.

__del__

The destructor takes deinit self:

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

Destructors can't raise. Trivial types can't define a destructor.

Nested functions

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

def outer(x: Int) -> Int:
def inner() unified {read} -> Int:
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