Variables

Introduction to Mojo variables.

A variable is a name that holds a value or object, and it can be either mutable and immutable—that is, it can either “mutate” (change) or it can’t.

Undeclared variables

Within a def function or a REPL environment, you can create a variable with just a name and a value. For example:

name = "Sam"

A variable declared without var or let is mutable (it defaults to var).

Note: Undeclared variables are not allowed in an fn function or as a struct field.

Mutable var and immutable let variables

You can declare a variable as mutable with var or as immutable with let. For example:

var name = "Sam"  # Mutable
let user_id = 42  # Immutable

The name variable is mutable, so you can change it later, but if you try to change user_id after it’s initialized, you’ll get a compiler error. (You can initialize the value later if you specify the type.)

Using var helps prevent runtime errors caused by typos. For example, if you misspell the name of an undeclared variable, Mojo simply instantiates a new variable using the misspelled name. But when all mutable variables must be first declared with var (which is the case inside an fn function), then misspellings such as the following are caught by the compiler:

var name = "Sam"
# Somewhere later...
nane = "Sammy"  # This is not allowed in an `fn` function

So, although using var is optional in a def function, the benefit is realized only when used inside an fn function, where the Mojo compiler will flag undeclared variables (such as the above nane) as unknown declarations.

Whereas, declaring an immutable let variable (also known as a “constant”) is useful in any situation where you want to avoid bugs that can occur when a value is supposed to remain the same but is accidentally changed anyway. By using let, the compiler catches these mistakes (in def and fn functions), so they don’t cause errors at runtime. Also, because the let value is guaranteed to not change at runtime, the compiler can make some performance optimizations.

Note: When using Mojo in a REPL environment, top-level variables (variables outside a function or struct) do not require var or let declarations.

Type annotations

Although Mojo supports dynamic variable types (it can infer a value type at runtime), it also supports static type annotations on variables. This enables strong compile-time type checking for variables, which can make your code more predictable, manageable, and secure (especially when combined with type checking in fn functions).

To specify the type for a variable, add a colon followed by the type name:

var name: String = "Sam"

This way, name can never be assigned a value that’s not a string (or that cannot be implicitly converted to a string).

Note: You must declare var or let to use type annotations.

If a type has a constructor with just one argument, you can initialize it in two ways:

var name1: String = "Sam"
var name2 = String("Sam")

Both of these lines invoke the same constructor to create a String from a StringLiteral.

Late initialization

Using type annotations allows for late initialization. For example, notice here that the z variable is first declared with just a type, and the value is assigned later:

fn my_function(x: Int):
    let z: Float32
    if x != 0:
        z = 1.0
    else:
        z = foo()
    print(z)

fn foo() -> Float32:
    return 3.14

Note: Late initialization works only if the variable is declared with a type.

Implicit type conversion

Some types include built-in type conversion (type casting) from one type into its own type. For example, if you assign a number to a String, it creates the string "1" instead of a compiler error:

var number: String = 1

As shown above, value assignment is just sugaring for a constructor call (which specifies the value type). So, this code uses the String constructor that takes an integer: __init__(inout self, num: Int).

This follows the logic of overloaded functions, because that’s exactly what’s happening here; assigning a number to a String variable is exactly the same as this:

var number = String(1)

Thus, if you call a function that requires an argument of a certain type (such as String), you can pass in any value as long as that value type can implicitly convert to the required type (using one of the type’s overloaded constructors).

For example, you can pass an Int to a function that expects a String, because String includes a constructor that takes an Int:

fn take_string(version: String):
    print(version)

fn pass_integer():
    let version: Int = 1
    take_string(version)

Variable scopes

Variables declared with var and let are bound by lexical scoping. This means that nested code blocks can read and modify variables defined in an outer scope. Conversely, an outer scope cannot read variables defined in an inner scope at all.

For example, the if code block shown here creates an inner scope where outer variables are accessible to read/write, but any new variables do not live beyond the scope of the if block:

def lexical_scopes():
    let num = 10
    var dig = 1
    if True:
        print("num:", num)  # Reads the outer-scope "num"
        let num = 20        # Creates new inner-scope "num"
        print("num:", num)  # Reads the inner-scope "num"
        dig = 2             # Edits the outer-scope "dig"
    print("num:", num)      # Reads the outer-scope "num"
    print("dig:", dig)      # Reads the outer-scope "dig"

lexical_scopes()
num: 10
num: 20
num: 10
dig: 2

The lifetime of the inner num ends exactly where the if code block ends, because that’s the scope in which the variable was defined.

This is in contrast to undeclared variables (those without the var or let keyword), which use function-level scoping (consistent with Python variable behavior). That means, when you change the value of an undeclared variable inside the if block, it actually changes the value for the entire function.

For example, here’s the same code but without the let declarations:

def function_scopes():
    num = 1
    if num == 1:
        print(num)   # Reads the function-scope "num"
        num = 2      # Updates the function-scope variable
        print(num)   # Reads the function-scope "num"
    print(num)       # Reads the function-scope "num"

function_scopes()
1
2
2

Now, the last print() function sees the updated num value from the inner scope, because undeclared variables (Python-style variables) use function-level scope (instead of lexical scope).