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:
| Marker | Arguments | Parameters |
|---|---|---|
// | No | Inferred |
/ | Positional-only | Positional-only |
* | Keyword-only | Keyword-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