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: Nightly
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
//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 reflect

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 Int.
inferred_type("Hello") # Value is Hello. Type is String.

Infer-only parameters can't be specified positionally:

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

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 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[DType.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[DType.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

Argument passing conventions

A passing 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 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! (moved)
# print(greeting) # Error because 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 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 __del__ and the existing argument in move constructors:

struct Resource:
var handle: Int

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

def main():
var data = ["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 ValueError -> Int:
# ...

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

thin

Declares that a function type doesn't capture values from its defining scope. A thin function is a function pointer and not a closure.

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

# `square` doesn't capture any values, so it is `thin`
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]

abi("C")

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

from std.ffi import OwnedDLHandle

def main() raises:
# On Linux. On macOS, use "libSystem.dylib", and so forth
var lib = OwnedDLHandle("libm.so")
var sqrt = lib.get_function[def(Float64) thin abi("C") -> Float64](
"sqrt"
)
print(sqrt(4.0)) # 2.0

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 because __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 default convention of a readable immutable reference. 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() {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

Function overloads

A function overload is one of two or more function declarations that share a name but differ in their signature. The compiler picks one of them at each call site. This is static dispatch: there's no runtime lookup. The choice is fixed when the call is type-checked.

An overload set is the collection of overloads the compiler considers at a call site. It contains the declarations that share the same name in the same scope.

Use overloads to give one operation more than one shape: different argument types, different argument counts, different keyword names, different self conventions, or different compile-time parameter signatures.

def add(x: Int, y: Int) -> Int:
return x + y

def add(x: String, y: String) -> String:
return x + y

def main():
print(add(1, 2)) # 3
print(add("Hi, ", "Mojo")) # Hi, Mojo

Where overload sets form

Each scope builds its own overload set:

  • Module scope. Declarations with the same name in the same module form one overload set.
  • Struct scope. Methods on a struct (including @staticmethod) form one overload set per method name.
  • Trait scope. Required and provided methods on a trait form one overload set per method name.

An overload set can't be extended across scopes. An import brings the name in as a non-function reference: you can't add another overload to it from your own module, and you can't redefine it. A local declaration that collides with an import produces an error.

To avoid the error, use an alias:

from some_package import add as imported_add

def add(x: Float64, y: Float64) -> Float64:
return x + y

# `add` resolves to the local definition.
# `imported_add` resolves to the imported one.

What the compiler considers

Overload resolution looks at:

  • The number, position, and keyword of each argument.
  • The type of each argument and each compile-time parameter.
  • The argument conventions on each argument.
  • Whether the candidate is an instance method or @staticmethod.
  • Whether a constructor is @implicit.

Overload resolution doesn't look at the return type or any other context surrounding the call.

Resolution rules

The compiler discards every candidate whose signature can't be satisfied by the call. It then compares the remaining candidates pairwise.

It applies the following rules in order until one wins. The compiler selects that candidate.

  1. Pick the candidate that uses fewer implicit conversions between arguments and parameters. An empty match against an *args argument counts as an implicit conversion, so an exact match beats it.

  2. Pick the candidate that doesn't bind non-empty variadic arguments. A signature without *args beats one whose *args argument receives at least one value.

  3. Pick the candidate with fewer mismatched argument conventions.

  4. Pick the candidate with a shorter parameter list. A function with no compile-time parameters beats one that declares a parameter. The parameter list also counts implicit parameters synthesized from argument types (for example, the unbound parameters of a SIMD[...] argument become implicit parameters on the function).

After fitness comparisons, the compiler applies two tiebreakers:

  1. Pick the candidate that is an instance method over a @staticmethod with the same name.

  2. Pick the candidate that is a non-@implicit constructor over an @implicit one.

If two candidates are equally good after these steps, the call is ambiguous. The compiler rejects it.

Overloading parameters

Functions can overload on compile-time parameters as well as on arguments:

def take_param[a: Int, b: Int]():
print("take_param[a: Int, b: Int]")

def take_param[a: Int, b: String]():
print("take_param[a: Int, b: String]")

def main():
take_param[1, 2]() # take_param[a: Int, b: Int]
take_param[1, "hi"]() # take_param[a: Int, b: String]

Overloading the self convention

A method can be overloaded by its self convention. The default call site uses an immutable reference for self, so ref self wins. To reach the var self overload, the caller transfers with ^ at the call site:

@fieldwise_init
struct Counter(Copyable):
var n: Int

def which(ref self) -> Int:
return 1

# Constrain `var self` to types where `^` actually transfers.
# For `TrivialRegisterPassable` types, `^` is a no-op. This
# overload would otherwise be silently unreachable.
def which(var self) -> Int where not conforms_to(
Self, TrivialRegisterPassable
):
return 2

def main():
var c1 = Counter(0)
print(c1.which()) # 1: default call uses an immutable self reference
var c2 = Counter(0)
print((c2^).which()) # 2: caller transfers self

Instance methods beat static methods

When both an instance method and a @staticmethod have the same name, a method-call expression picks the instance method (rule 5):

struct StaticOverload:
def __init__(out self):
pass

def foo(mut self):
print("instance method")

@staticmethod
def foo():
print("static method")

def main():
var a = StaticOverload()
a.foo() # instance method

To call the static method explicitly, use the type name:

StaticOverload.foo() # static method

Variadic candidates lose ties

A signature without *args beats a variadic signature when both match the call, because the variadic version costs one implicit conversion for the empty pack (rule 1) or, with values supplied, loses on rule 2:

def take(x: Int):
print("take(x: Int)")

def take(*xs: Int):
print("take(*xs: Int)")

def main():
take(1) # take(x: Int)
take(1, 2, 3) # take(*xs: Int): the only match.

Ambiguous calls

When two candidates are equally good, the call fails:

struct MyString:
@implicit
def __init__(out self, s: String):
pass

struct YourString:
@implicit
def __init__(out self, s: String):
pass

def foo(name: MyString):
print("MyString")

def foo(name: YourString):
print("YourString")

def main():
# Error because the call is ambiguous: both overloads need exactly
# one implicit conversion from `String`
foo("Hello")

Resolve ambiguity by casting at the call site:

def main():
foo(MyString("Hello")) # MyString
foo(YourString("Hello")) # YourString

Literals can trigger the same kind of ambiguity. An IntLiteral converts to both Int and Float64 at equal cost, so a call like take_param[1, 2]() against the two overloads below is ambiguous:

def take_param[a: Int, b: Int]():
pass

def take_param[a: Int, b: Float64]():
pass

def main():
# Error because `IntLiteral` converts to both `Int` and `Float64`
# at equal cost; the compiler can't pick a winner
take_param[1, 2]()

Resolve by binding one parameter to a literal type that only one overload accepts (for example, take_param[1, "hi"]() against a b: String overload), by adding a non-overlapping parameter, or by giving the overloads distinct names.

Return types don't disambiguate

Two overloads that differ only in their return type are indistinguishable to overload resolution:

def parse(s: String) -> Int:
return 0

# Error because `parse` cannot overload on return type only;
# the differing return type doesn't form a new overload
def parse(s: String) -> Float64:
return 0.0

Use different argument types, an extra parameter, or a different function name instead.

raises doesn't disambiguate

Two functions that differ only in whether they raises have the same signature for overload-set purposes. The compiler rejects the second declaration:

def maybe_raise(x: Int) -> Int:
return x

# Error because `maybe_raise` already has this signature;
# `raises` isn't part of the signature for overload purposes
def maybe_raise(x: Int) raises -> Int:
raise Error("nope")

If both behaviors are needed, give them distinct names or make the raising version take a different argument shape.

Best practices

  • Use overloads when each version implements the same operation on a different shape of input. Don't overload to mean different things under the same name.
  • Don't rely on two @implicit constructors being reachable from the same source type. Cast at the call site or make one constructor non-@implicit.
  • Prefer overloading on argument type over overloading on convention. Convention-based overloads work, but they're easy to misread.
  • When an overload set should accept many types, write one parameterized function constrained with where instead of many near-duplicates. The compiler picks the concrete signature when both are present (rule 4).
  • Don't define a function with the same name as one you imported. The compiler rejects it. Import under an alias when you need both names.