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: 1.2.0
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 overload reference

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 deinitializer 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 deinitializer 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]()

To resolve, remove the ambiguity or remove the overload by renaming one version.

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 initializers being reachable from the same source type. Cast at the call site or make one initializer 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.