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, " + nameThe simplest function has a name, empty parentheses, and a body:
def do_nothing():
passFunction names
All function names must be valid identifiers.
You may use keywords as function names by escaping them with backticks, as you would for variable names:
def `import`():
print("In `import`")
def main():
`import`() # In `import`Function signatures
A function signature incorporates many pieces, all optional except the parentheses and the colon:
def clamp[T: Comparable & ImplicitlyCopyable](val: T, lo: T, hi: T) -> T:
if val < lo:
return lo
if val > hi:
return hi
return valIn the preceding example:
clampis the function name.[T: Comparable & ImplicitlyCopyable]is its parameter list. Parameters are compile-time values. The compiler resolves them before a function runs.(val: T, lo: T, hi: T)is the argument list. Arguments are runtime values the caller passes in.-> Tis the return type.- The remaining content is the function body, a block of code that executes when the function is called.
Arguments are runtime values in parentheses. Each argument has a name and type. In other languages, these are called parameters, but in Mojo they're arguments to avoid confusion with compile-time parameters.
def add(a: Int, b: Int) -> Int:
return a + bParameters are compile-time values in square brackets. They appear between the function name and the argument list. Parameters are optional, but if you include one, you must use the square brackets at the call site for any parameter that is not defaulted or inferred:
def repeat[count: Int](msg: String):
for _ in range(count):
print(msg, end=" ")
print()
def main():
repeat[3]("Hello, world!") # Hello, world! Hello, world! Hello, world!Every parameter uses a type annotation. The compiler rejects parameters without one:
def incorrect[T](): # error: parameters must always have a type
passOther elements: A function that can raise an error declares raises
before the return arrow. A function with compile-time constraints uses
where clauses. Parameterized functions use a parameter list.
Markers
Three markers divide parameter and argument lists into zones controlling how callers can pass values. Each marker has specific rules about how values in its zone can be passed:
| Marker | Arguments | Parameters |
|---|---|---|
// | No | Inferred |
/ | Positional-only | Positional-only |
* | Keyword-only | Keyword-only |
The inferred-only marker (//, parameters only)
// markers separate inferred parameters from named ones. The compiler
deduces inferred parameters from call site arguments. You can't pass these
elements in the square parameter brackets.
In the following example, the compiler infers T (which is String
here) from the value argument. count defaults to 3:
def splat_list[T: Copyable, //, count: Int = 3](value: T) -> List[T]:
return List[T](length=count, fill=value)
def main():
print(splat_list("hello")) # ["hello", "hello", "hello"]
print(splat_list[2]("hello")) # ["hello", "hello"]This example has two call sites.
The first call site doesn't need to use square brackets. The parameters are
gathered from inference and a default value. The second uses a parameter to
specify count. Infer-only parameters never appear at call-sites.
The positional-only marker (/)
/ marks everything before it as positional-only. Callers must pass
these values by position. Named arguments are not allowed:
def div(a: Int, b: Int, /):
return a // b
div(10, 3) # ok
div(a=10, b=3) # errorThe keyword-only marker (*)
Every argument or parameter after a * marker is keyword-only. Callers
must pass values by name.
def configure(*, verbose: Bool, retries: Int):
...
configure(verbose=True, retries=3) # ok
configure(True, 3) # errorA *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 argumentMarker order rules
Markers must appear in this order: //, then /, then *. Each can
appear once. / can't be first in the list, and * can't be last.
Default values
Both parameters and arguments can assign default values, allowing you to eliminate mentioning them at the call site:
def connect(host: String = "www.modular.com", port: Int = 80):
print(f"Connecting to {host}:{port}")
def main():
connect() # Connecting to www.modular.com:80
connect(port=8080) # Connecting to www.modular.com:8080Default value ordering rules
Once you provide a default value, every following parameter or argument must also have a default. For example:
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 other_function(x: Int, y: Int = 0, z: Int):
# return x + y + z
def main():
print(my_function(1)) # 1
print(my_function(1, 2)) # 3
print(my_function(1, 2, 3)) # 6In this example, count defaults to 3:
def fill[T: ImplicitlyCopyable, count: Int = 3](value: T) -> List[T]:
return List[T](length=count, fill=value)
def main():
print(fill[String]("🔥")) # [🔥, 🔥, 🔥]
print(fill[String, 2]("🔥")) # [🔥, 🔥]Keyword-only parameters and arguments are exempt from the ordering rule.
They can mix required and optional freely as the compiler can always tell
which is which from the presence of the * marker:
# This is valid because 'verbose' and 'retries' are keyword-only.
def configure(*, retries: Int = 3, verbose: Bool):
passFunction constraints
Constraints with where clauses
Every parameter can use a where clause to constrain its value or type.
For example, this process function requires n to be a power of two,
ensuring the SIMD vector extent is valid:
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.0A where clause can also be placed at the end of the function 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():
result = compare(5, 10)
print(result) # -1 (LESS_THAN)
result = compare(10, 5)
print(result) # 1 (GREATER_THAN)
result = compare(7, 7)
print(result) # 0 (EQUAL)
# Error: constraint violation: List is not Comparable
# result = compare([1, 2], [1, 2, 3])
# print(result) # -1 (LESS_THAN)Constraints with trait conformance
This function constrains parameters based on conformance. The following
example requires Equatable (to compare values) and Copyable (required
by the list):
def my_contains[T: Copyable & Equatable](value: T, items: List[T]) -> Bool:
for item in items:
if item == value:
return True
return False
def main():
numbers = [1, 2, 3, 4, 5]
print(my_contains(3, numbers)) # True
print(my_contains(6, numbers)) # FalseConstraints with arguments
Using where clauses on arguments isn't allowed:
# error: where clauses can only be used for
# compile time parameters
def bad(x: Int where x > 0):
passArgument conventions
An argument convention controls how a value passes to a function. You write the convention 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 *= 2mut arguments can't have default values:
# error: 'mut' arguments may not have defaults
def bad(mut x: Int = 0):
passvar
The function receives an owned copy of the value.
- If the original call is transferred, the caller's value is moved into the function and becomes inaccessible to the caller.
- If the original call is copied, the caller's value is copied into the function and remains accessible to the caller. The function can modify its copy, but those changes don't affect the caller's value.
def consume(var s: String):
s += "!"
print(s)
def main():
var greeting = "Hello"
consume(greeting) # Hello!
print(greeting) # Hello
consume(greeting^) # Hello!
# print(greeting) # error: 'greeting' is uninitialized after moveout
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() # x is inferred as Int with value 42
print(x) # 42A function can't use both an out argument and a -> Type return:
# error: function cannot have both an 'out' argument
# and an explicit result type
def bad(out result: Int) -> Int:
result = 0out arguments can't have defaults and can't be variadic.
deinit
The function takes ownership and destroys the value. This convention is
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, and trivial types can't define a destructor.
ref
The ref convention passes a reference, often 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) # oneDefault convention
When you don't write a convention, the argument is immutable and borrowed. The caller keeps ownership, and the function can't modify the value.
def length(s: String) -> Int:
return len(s)Variadic arguments
Homogeneous variadics
A * before the argument name accepts any number of
positional arguments of the same type:
def sum_all(*values: Int) -> Int:
var total = 0
for v in values:
total += v
return totalVariadic packs
A * before both the name and the type annotation creates
a variadic pack that accepts arguments of different types:
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.14Variadic restrictions
A function can have at most one *args and variadic arguments can't use
default values:
# error: variadic arguments may not have defaults
def bad(*args: Int = 0):
passout arguments can't be variadic:
# error: 'out' convention may not be variadic
def bad(out *results: Int):
passEffects
Effects appear after the closing parenthesis and before
->. They describe side effects of the function.
raises
Declares that the function can raise an error. You can optionally specify the error type:
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. If multiple types can be raised, the compiler reports an error.
Return type
The -> token introduces the return type. It appears after
any effects:
def square(x: Int) -> Int:
return x * xIf you omit ->, the function returns None.
Special methods
Certain method names have enforced signatures. The compiler checks argument count, conventions, and return types for these names.
__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 = yWithout out self, the compiler rejects the struct's method:
# error: __init__ method must return Self type
# with 'out' argument
def __init__(self):
passCopy constructor
A struct's 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.yThe copy argument must use the read convention (the default). Copy
constructors can't raise. Trivial types can't define a copy constructor
because they're always trivially copyable.
Move constructor
A struct's 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.yThe 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 because they're always trivially destroyable.
Nested functions
Functions can be defined inside other functions. A nested function automatically captures values from its enclosing scope:
def outer(x: Int) -> Int:
def inner() -> Int:
return x + 1
return inner()The compiler resolves nested function bodies immediately so captures bind correctly.
Static methods
A @staticmethod decorator makes a struct method callable without an
instance. Static methods don't take a self argument. They operate
on the type, not on instances of the type:
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.141592653589793Was this page helpful?
Thank you! We'll create more content like this.
Thank you for helping us improve!