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) # IdenticalWhen 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__' methodIn 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 initializedThis 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 initializedField 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_ownerAny 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 fieldsThe 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 tooWhen 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 nameAny 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` hereLike 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 fieldDestroying 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 goneWith 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?
Thank you! We'll create more content like this.
Thank you for helping us improve!