Skip to main content

Deep dive - Instance initialization

Mojo tracks two kinds of initialization for structs: fieldwise and logical.

Fieldwise initialization means the struct’s storage holds valid values for each field. Logical initialization means the instance as a whole is in a valid, usable state.

Understanding the difference between these two forms of initialization is essential for working with structs correctly.

The basics

You create struct instances by calling the __init__() constructor:

struct Person:
    var name: String
    var age: Int

    fn __init__(out self, name: String, age: Int):
        self.name = name
        self.age = age

fn main():
    var me = Person("Alice", 30)

Calling Person("Alice", 30) is syntactic sugar for calling the constructor directly:

var me: Person
me = Person.__init__("Alice", 30) # Identical

When constructing a Person, the compiler first allocates stack space for the instance me, then __init__() initializes that space.

Fieldwise vs logical initialization

Initializing a struct by assigning values directly to its fields may populate the data, but it does not make the instance usable:

struct Person(Writable):
    var age: Int
    var height: Int

    ...

fn main():
    var me: Person
    me.age = 25
    me.height = 6
    print(me)  # Error
    # error: 'me' used with all fields manually initialized
    # but without calling an '__init__' method

In this example, all fields contain valid values, but the instance is still not considered initialized.

Mojo tracks two distinct initialization states:

  • Fieldwise initialization means each field contains valid data and the instance’s storage has been populated.
  • Logical initialization means the instance as a whole is valid and its __init__() method has run.

Fieldwise initialization alone isn't enough. The compiler requires logical initialization before an instance can be used:

var me = Person(25, 6)  # Now OK: __init__() ran
print(me)

Calling __init__() makes an instance logically initialized. This process isn’t just about assigning data. It ensures that any required initialization logic ran and that the instance is safe to use.

Inside __init__()

Within __init__(), the incoming self instance is considered logically initialized, but its fields are still uninitialized:

fn __init__(out self, name: String, age: Int):
    # At this point:
    # - Logically initialized (self is valid as an instance)
    # - Fieldwise uninitialized (fields have no values yet)

    self.name = name
    self.age = age

    # Now both logically and fieldwise initialized

This distinction is intentional. Entering __init__() establishes the instance itself, but it’s your responsibility to populate its fields.

You must initialize every field in __init__():

fn __init__(out self, name: String, age: Int):
    self.name = name
    # Error: field 'age' not initialized in __init__

The __init__() signature doesn’t have to mirror the struct’s fields. You can use parameters, constants, or external values to initialize those fields:

# Parameters can be used to initialize fields
self._store = List[T](capacity=Count)

# Constants can be used to initialize fields
self.string = ""

# External values can be used to initialize fields
from math import pi
self.default_angle = pi / 2.0
self.uuid = MyUUIDImplementation.uuid()

You can’t call methods until all fields are initialized:

fn __init__(out self, name: String):
    self.greet()      # Error: self not fully initialized
    self.name = name
    self.greet()      # OK: all fields initialized

Field initialization is restricted to __init__() methods. Regular functions can’t initialize individual fields of an out argument, but __init__() methods can.

Moving from fields

Moving a value out of a field deinitializes that field:

struct Person(Writable):
    var name: String
    var age: Int

    fn __init__(out self, name: String, age: Int):
        self.name = name
        self.age = age

fn main():
    var me = Person("Connor", 25)
    var name_owner = me.name^  # Move value from me.name.
    print(me)     # Error: use of uninitialized value 'me'

After the move, the instance is still logically initialized, but it’s only partially fieldwise initialized. Mojo doesn’t allow using instances with uninitialized fields.

To make the instance usable again, you must reinitialize the moved-from field:

var me = Person("Connor", 25)
var name_owner = me.name^
me.name = "John"      # Reinitialize field
print(me)             # OK
use_value(name_owner) # Now, use name_owner

Any moved field requires a new value before you can use the instance again. Even if all fields are moved, the instance itself remains logically initialized.

Mojo's default destructor

__del__() is Mojo’s default destructor (or deinitializer). While it runs, the instance remains logically initialized, and it becomes logically deinitialized when the method completes:

@fieldwise_init
struct Contact:
    var name: String
    var email: String

    fn __del__(deinit self):
        # `self` is initialized
        print("destroying contact")
        # `self is deinitialized

fn main():
    var me = Contact("Alice", "alice@example.com")
    # Last use of `me` (and end of scope)
    # Prints "destroying contact" along with a warning
    # since the instance was never used after assignment.

Destructors and moving fields

Destructors are the only place where you can safely move values out of an instance’s fields without having to reinitialize the field before its next use.

This special rule isn’t about where the code lives. It’s about guarantees:

fn __del__(deinit self):
    var name = self.name^  # OK: can take ownership of fields

The compiler knows that destructors like __del__() are the final use of an instance. Because of this, it allows you to move values out of fields without reinitializing them.

This ensures that fields are only moved out of struct instances when the compiler can guarantee the instance is at the end of its lifetime.

Like instances, struct fields use ASAP destruction within deinit methods. For example:

struct S:
   var a: String
   var b: String

   fn __del__(deinit self):
       # Mojo calls a.__del__() here.
       use(b)
       # Mojo calls b.__del__() here.

The deinit convention

A destructor’s secret sauce is the deinit convention. Because of this, deinit self works in named destructors as well as __del__():

struct Parent:
    fn consume(deinit self):
        # `consume` is a named destructor
        # Can move values out of fields here too

When you use the deinit self convention in a method, the compiler recognizes the method as a destructor and lets you move a field without reinitializing it:

struct Parent:
    fn consume(deinit self):
        """valid destructor"""
        var name = self.name^
        print("Name:", name) # Prints name

Any struct method that uses deinit self as its first argument is a destructor. As a destructor, you can use this consume method for explicit destruction types.

Explicit and implicit destructors

The __del__() method is the core of implicit destruction types. It’s Mojo’s default destructor and works hand in hand with ImplicitlyDestructible, a trait that all structs inherit by default. The compiler calls __del__() automatically when it detects the final use of an implicitly destructible struct value.

As the name suggests, explicit destruction requires you to call a destructor. An explicitly destroyed type must define at least one custom destructor, such as the consume() method shown in the previous section. The compiler make sure that the final use of an explicitly destroyed value will be a call to one of its custom destructors.

Using deinit with other arguments

The deinit convention isn’t limited to self. You can write methods and functions that destruct other instances:

struct Pair:
    fn destroy_other(self, deinit other: Self):
        # Can take from fields of `other` here

Like deinit self, the deinit convention in this example tells the compiler that other is tagged for destruction.

Using deinit means other is logically deinitialized at the end of the method. Because of this, it’s safe to move values out of other, since the instance’s lifetime is guaranteed to complete.

Normal methods and moving fields

In ordinary methods, you can’t move values out of a struct’s fields without reinitializing them:

@fieldwise_init
struct MyStruct:
    var name: String

    fn move_field(mut self, var new_name: String):
        var name = self.name^ # Error
        # error: 'self.name' is uninitialized at the implicit return
        #        from this function
        print("Name:", name)

fn main():
    var instance = MyStruct("Ken")
    instance.move_field("Scott")
    print("Name:", instance.name) # Prints: "Name: Scott"

To move safely, reinitialize the instance’s fields before the end of scope. The following update fixes the bug by reinitializing the field:

fn move_field(mut self, var new_name: String):
    var name = self.name^
    print("Name:", name)  # Prints name
    self.name = new_name^ # reinitialize the name field

Destroying instances

Unless you opt into explicit destructors, Mojo uses implicit destruction. Both explicit and implicit destructors use ASAP (“As Soon As Possible”) destruction:

fn example():
    var x = SomeType()
    use(x) # last use in scope
    # x destroyed here, immediately after last use

    more_code()  # x is already gone

With ASAP, Mojo destroys instances immediately after their last use, not at the end of scope. This can happen even in the middle of an expression.

This differs from many languages with compiler-managed lifecycles, where destruction happens at end of scope. Mojo’s ASAP destruction enables better tail call optimization, but it also means destructors can run earlier than you might expect.

Mojo also supports explicitly-destroyed types, which require you to make an explicit call to a destructor method. For more information, see Explicitly-destroyed types.

Mojo tracks the lifetimes of values in your code. It knows when you move out of a value using the transfer operator (^), or when you call a deinit method that explicitly destroys it. If the compiler finds a live value that hasn’t otherwise been cleaned up, it uses the implicit destructor (__del__(deinit self...)) to destroy it.

If a type doesn’t have an implicit destructor because it requires explicit destruction, the compiler emits an error, as indicated by the @explicit_destroy decorator.

While the compiler checks that a destructor is called before an instance goes out of scope, you’re responsible for choosing when that happens. The key differences are:

  • Destruction doesn’t automatically happen at the point of last use.
  • The explicit destructor isn’t named __del__().

Object initialization summary

Creating, using, and destroying instances in Mojo follows a consistent set of rules based on initialization state.

Create instances by calling __init__(). You must initialize every field in the initializer, especially if you plan to use the instance within the method scope. You can’t call methods on a struct unless all its fields are initialized.

You must logically initialize an instance before using it, which means calling the initializer. You must also initialize fields before accessing them. Partially initialized instances can’t be used in many contexts, including method calls and return values.

For implicitly destroyed structs, the compiler inserts a destructor call after the instance’s last use. For explicitly destroyed values, you must call a destructor before the instance goes out of scope. Destruction is compiler-checked, but you choose when it happens. The value’s final use in scope must be that explicit destructor call.

Field operations are also constrained. Fieldwise initialization must happen inside the __init__() constructor. You can move values out of a field only when the compiler can prove that the instance won’t be used again, or that the field will be reinitialized before the end of a normal method.

This split between logical and fieldwise initialization gives Mojo fine-grained control over instance lifetimes, while ensuring that initialization, use, and destruction remain safe and explicit.

Was this page helpful?