Functions

Introduction to Mojo fn and def functions.

As mentioned in Language basics, Mojo supports two types of functions: def and fn functions. You can use either declaration with any function, including the main() function, but they have different default behaviors, as described on this page.

We believe both def and fn have good use cases and don’t consider either to be better than the other. Deciding which to use is a matter personal taste as to which style best fits a given task.

We believe Mojo’s flexibility in this regard is a superpower that allows you to write code in the manner that’s best for your project.

Note: Functions declared inside a struct are called “methods,” but they have all the same qualities as “functions” described here.

def functions

The def function provides the same dynamism and flexibility as a Python def function. For example, this function works the same in Python and Mojo:

def greet(name):
    greeting = "Hello, " + name + "!"
    return greeting

In Mojo, you also have the option to specify the argument type, return type, and variable mutability like this:

def greet(name: String) -> String:
    let greeting = "Hello, " + name + "!"
    return greeting

This way, the compiler ensures that name is a string, the return type is a string, and the greeting variable cannot mutate.

Here’s everything to know about def:

  • Arguments don’t require a declared type.

    Undeclared arguments are actually passed as an object, which allows the function to receive any type (Mojo infers the type at runtime).

  • Return types don’t need to be declared and also default to object.

  • Arguments are mutable (usually passed by value, using the owned argument convention).

    If an argument is an object type, it’s received as a reference, following object reference semantics.

    If an argument is any other declared type, it’s received as a value (using the owned argument convention).

  • Variables don’t need to be declared as mutable (var) or immutable (let); they default to mutable.

The object type

If you don’t declare the type for an argument or return value in a def, it becomes an object, which is unlike any other type in the standard library.

The object type allows for dynamic typing because it can actually represent any type in the Mojo standard library, and the actual type is inferred at runtime. (Actually, there’s still more to do before it can represent all Mojo types.) This is great for compatibility with Python and all of the flexibility that it provides with dynamic types. However, this lack of type enforcement can lead to runtime errors when a function receives or returns an unexpected type.

For compatibility with Python, object values are passed using object reference semantics. As such, the object type is not compatible with the argument conventions that enforce value semantics. So, be careful if using object values alongside other strongly-typed values—their behavior might be inconsistent because object is the only type in the standard library that does not conform to full value semantics.

fn functions

The fn function provides strict type checking and additional memory safety. It basically forces you to write the optional things in def, and it ensures that you don’t accidentally mutate received arguments. For example, here’s the same function from above using fn:

fn greet(name: String) -> String:
    let greeting = "Hello, " + name + "!"
    return greeting

As far as a function caller is concerned, def and fn functions are interchangeable. That is, there’s nothing a def can do that an fn can’t (and vice versa). The difference is that, compared to a def function, an fn function is more strict on the inside.

Here’s everything to know about fn:

  • Arguments must specify a type (except for the self argument in struct methods).

  • Return values must specify a type, unless the function doesn’t return a value.

    If you don’t specify a return type, it defaults to None (meaning no return value).

  • By default, arguments are received as an immutable reference (values are read-only, using the borrowed argument convention).

    This prevents accidental mutations, and permits the use of non-copyable types as arguments.

    If you want a local copy, you can simply assign the value to a local variable. Or, you can get a mutable reference to the value by declaring the inout argument convention).

  • Variables must be declared as mutable (var) or immutable (let).

  • If the function raises an exception, it must be explicitly declared with the raises keyword. (A def function does not need to declare exceptions.)

By enforcing these type checks, using the fn function helps avoid a variety of runtime errors. It also improves performance compared to the dynamic typing in a def function, because there’s no overhead processing required to figure out what data types to use at runtime—the types are fixed at compile time.

Optional arguments

An optional argument is one that includes a default value, such as the exp argument here:

fn pow(base: Int, exp: Int = 2) -> Int:
    return base ** exp

fn use_defaults():
    # Uses the default value for `exp`
    let z = pow(3)
    print(z)

However, you cannot define a default value for an argument that’s declared as inout.

Keyword arguments

You can also specify argument values using the argument names:

fn pow(base: Int, exp: Int = 2) -> Int:
    return base ** exp

fn use_keywords():
    # Uses keyword argument names (with order reversed)
    let z = pow(exp=3, base=2)
    print(z)

Note: Mojo currently includes only partial support for keyword arguments, so some features such as keyword-only arguments and variadic keyword arguments (e.g. **kwargs) are not supported yet.

Overloaded functions

If a def function does not specify argument types, then it can accept any data type and decide how to handle each type internally. This is nice when you want expressive APIs that just work by accepting arbitrary inputs, so there’s usually no need to write function overloads for a def function.

On the other hand, all fn functions must specify argument types, so if you want a function to work with different data types, you need to implement separate versions of the function that each specify different argument types. This is called “overloading” a function.

For example, here’s an overloaded add() function that can accept either Int or String types:

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

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

If you pass anything other than Int or String to the add() function, you’ll get a compiler error. That is, unless Int or String can implicitly cast the type into their own type. For example, String includes an overloaded version of its constructor (__init__()) that accepts a StringLiteral value. Thus, you can also pass a StringLiteral to a function that expects a String.

When resolving an overloaded function call, the Mojo compiler tries each candidate function and uses the one that works (if only one version works), or it picks the closest match (if it can determine a close match), or it reports that the call is ambiguous (if it can’t figure out which one to pick).

If the compiler can’t figure out which function to use, you can resolve the ambiguity by explicitly casting your value to a supported argument type. For example, in the following code, we want to call the overloaded foo() function, but both implementations accept an argument that supports implicit conversion from StringLiteral. So, the call to foo(string) is ambiguous and creates a compiler error. We can fix it by casting the value to the type we really want:

@value
struct MyString:
    fn __init__(inout self, string: StringLiteral):
        pass

fn foo(name: String):
    print("String")

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

fn call_foo():
    let string = "Hello"
    # foo(string) # This call is ambiguous because two `foo` functions match it
    foo(MyString(string))

When resolving an overloaded function, Mojo does not consider the return type or other contextual information at the call site—only the argument types affect which function is selected.

Overloading also works with combinations of both fn and def functions. For example, you could define multiple fn function overloads and then one or more def versions that don’t specify all argument types, as a fallback.

Note: Although we haven’t discussed parameters yet (they’re different from function arguments, and used for compile-time metaprogramming), you can also overload functions based on parameter types.