Skip to main content

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

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 val

In the preceding example:

  • clamp is 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.
  • -> T is 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 + b

Parameters 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
    pass

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

MarkerArgumentsParameters
//NoInferred
/Positional-onlyPositional-only
*Keyword-onlyKeyword-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)   # error

The 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)                  # 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

Marker 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:8080

Default 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))  # 6

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

Function 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.0

A 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))  # False

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

Argument 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 *= 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 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 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()  # x is inferred as Int with value 42
    print(x)            # 42

A 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 = 0

out 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)  # one

Default 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 total

Variadic 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.14

Variadic 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):
    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 ->. 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 * x

If 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 = y

Without out self, the compiler rejects the struct's method:

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

Copy 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.y

The 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.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 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.141592653589793

Was this page helpful?