Mojo🔥 Object Initialization

2025-07-28

Like many programming languages, in Mojo users can declare new types using the struct keyword. Broadly speaking, Mojo’s object model is perhaps closest to that of C++. However, that’s an imperfect analogy. In this article, I want to drill into one particular aspect of Mojo’s object model: how an object’s initialization status is treated by the Mojo compiler throughout its lifecycle.

My goal with this exploration is to describe the semantics of Mojo as they exist today, to facilitate broader understanding of this nuanced part of Mojo’s design, and provide a basis for shared understanding that elevates discussion about what Mojo might do differently in the future.

This post explores some of the nuances of how Mojo objects are created and destroyed, and tries to articulate an intuitive understanding of how the Mojo compiler “sees” an object instance. Along the way, you’ll learn about the Frankenstein 🧟 special nature of __init__() methods, tips like how to move out of struct fields correctly, and some thoughts on things Mojo could do differently in the future.

Readers are assumed to have familiarity with the basics of Mojo and object oriented programming. To learn more about Mojo, read the docs.

Mojo Object Construction: The Right Way

To begin, lets consider a simple example of what creating a Mojo object looks like today. Here’s a simple user-defined type in Mojo:

struct Foo:
	var x: Int
	var y: Int

	fn __init__(out self, x: Int, y: Int):
		self.x = x
		self.y = y

Following Mojo’s Python roots, Mojo requires that we declare an __init__() method. This method is special, because it’s what Mojo will call when constructing new instances of Foo:

# ...

fn main():
	var foo = Foo(5, 10)

	print(foo.x)

In the example above, Foo(5, 10) is syntax sugar for a call to the __init__() constructor method. To make that clear, lets rewrite the example to make the __init__() call explicit:

# ...

fn main():
	var foo: Foo
	
	foo = Foo.__init__(5, 10)

	print(foo)

The code behaves the same as the previous example, but now the order of operations are clearer:

Stack allocate space to hold a Foo, called foo.
Initialize that stack allocated memory by calling __init__().

Now that we have a better understanding of how Mojo expects us to construct an object, lets start interrogating this model a little, to get a better sense of some of its consequences.

Trying Field-Wise Object Initialization

One question we could ask is: why __init__()? We saw above it was possible to declare a variable separate from calling __init__() — what happens if we declare a variable and try to use it immediately? Let’s try it:

# ...

fn main():
	var me: Person

	print(me)

As we might have guessed, Mojo doesn’t accept this code. There is no notion of “default” initialization in Mojo. If you declare a variable, you must explicitly initialize it.

So just declaring a variable doesn’t work. But a struct is just a collection of fields, right? Could we simply initialize the fields directly? Let’s try that instead:

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

	# ...

fn main():
	var me: Person
	me.age = 25
	me.height = 6

	print(me)

A reasonable intuition would be that me is now fully initialized. But that isn’t quite how Mojo sees it:

ℹ️ As it turns out, Mojo is perfectly happy to let us initialize and read the fields of an overall object—as long as we don’t try to use the overall object.
struct Person:
	var age: Int
	var height: Int

fn main():
	var me: Person
	me.age = 25
	me.height = 6

	print(me.age)

So what’s going on here? Why does Mojo seem to require that we use an __init__() method to construct an object? To understand the answer to that question, we have to clarify something we’ve glossed over up until this point: what is an “object”, exactly?

An intuitive definition might be than an object is a chunk of memory, subdivided into segments storing the value of each struct field. For our Person type, we might visualize an object of type Person as a 16-byte region, with two 8-byte segments storing the age and height fields:

This memory-oriented view is a useful basis, but it isn’t a complete picture.

As programmers, we’re used to thinking in terms of memory: “an object is initialized if all of its bytes are initialized” feels like a reasonable definition. You might call this intuitive definition byte-wise initialization.

But in Mojo there is an aspect of initialization that isn’t visible at runtime, a useful fiction maintained and used by the compiler: that of logical initialization (aka. “whole object” initialization).

Byte-Wise Initialization vs Logical Initialization

Let’s consider a simple variable declaration:

	var me: Person

The byte-wise view is that all of the fields of me start out uninitialized, and that consequently the object as a whole is uninitialized:

And, in the byte-wise view, all that should be necessary to initialize the object is to write values to the fields:

	me.age = 25
	me.height = 6

— and that by initializing all the fields, consequently the object as a whole would be considered to be initialized:

But that’s not how Mojo sees things. In reality, in Mojo, just because all the fields of an object are initialized, that doesn’t mean the “whole object” is initialized; a more accurate visualization would be that, while the fields are initialized, the “Person”-ness of the object still isn’t initialized:

And now we arrive at a key point about objects in Mojo: Object construction in Mojo means that specific initialization logic (from an __init__() method) has been executed. It’s not enough to initialize the data stored in the fields and get an object that “looks” valid from the byte-wise perspective; you must also guarantee that associated object logic has been run as well.

This is what makes the __init__() method special: __init__() makes the Mojo compiler consider an object as having been logically initialized — the “whole object” is thereafter valid. It’s the zap ⚡ from an __init__() call that brings an object to life like Frankenstein’s monster 🧟.

Taken as a whole, our mental model of field-wise object initialization in Mojo might look something like:

Taking from Initialized Objects

So far we’ve concentrated on trying to understand how an uninitialized object becomes initialized. To round out our intuition, lets consider a different case: Suppose we have a fully initialized object, but we deinitialize one of its fields by taking ownership of that field’s value:

Note that in the example below, we’ve now got a name: String field, which unlike Int, is not trivially copyable, so moving from it leaves behind uninitialized data.
struct Person(Writable):
	var name: String
	var age: Int

    # ...

fn main():
	var me = Person("Connor", 25)

	# Move out of me.name
	var name = me.name^

	print(me)
	

In the example above, me starts out fully initialized, but by moving out of name, the object becomes partially initialized; visualized:

When we try to use me as part of print(me), the compiler—correctly—errors, informing us that we’re attempting to use an uninitialized value.

Note that the object “as a whole” is still considered logically “initialized” even though one of its fields is not initialized. One way we can see that that is true is that the compiler still tries to deinitialize an object by calling its __del__() method, even if some or even all of it’s fields have been moved from:

Here we introduce a Contact type, whose fields are all non-trivially-copyable.
@fieldwise_init
struct Contact:
	var name: String
	var email: String

	fn __del__(owned self):
		print("deinitializing", self.name)

fn main():
	var me = Contact("Connor", "connor@example.org")

	# Move out of only `me.name`
	var name = me.name^
	
@fieldwise_init
struct Contact:
	var name: String
	var email: String

	fn __del__(owned self):
		print("deinitializing", self.name)

fn main():
	var me = Contact("Connor", "connor@example.org")

	# Move out all fields of `me`
	var name = me.name^
	var email = me.email^
	

The error message refers to the “overall value” that has been prevented from being destroyed due to the partial move. The Mojo compiler rejects this code because me is only partially initialized at the point it goes out of scope and the compiler attempts to insert a __del__() method call.

Even when we moved from all of the fields of Contact, the object might be “byte-wise deinitialized”, but the “Contact”-ness of the object is still initialized:

Like with __init__() methods, deinitializing an object in Mojo isn’t just about dealing with the data stored in the object’s fields, it also means running any custom deinitialization logic from the __del__() method. However, the __del__() method (like any other method) is only valid to call if the object is fully initialized.

One way we can solve the error is by re-initializing the name field with a new value:

struct Person(Writable):
	# ...

	fn __del__(owned self):
		print("deinitializing", self)

fn main():
	var me = Person("Connor", 25)

	# Move out of me.name
	var name = me.name^

	# Provide a new value for me.name
	me.name = "John"

However, that solution has the limitation that it requires the user to “swap in” a new value to replace the one they took, Indiana Jones style. In the next section, we’ll learn about a low-level feature that provides a work-around for the problem shown above.

Omitting Destructor Calls

Suppose, like in the previous example, we have an initialized object, but for some reason we want to prevent its destructor from running. In general, you should not need to do thisthere are likely better ways to model things.

...but if you really need this capability, what can you do?

Uncommon but powerful, the __disable_del keyword causes the compiler to “pretend” that a value has been logically deinitialized. This enables us to leave objects partially initialized, among other useful low-level tricks.

Continuing on from the previous section, let’s consider a code example where we’re trying to take a value out of a field of an object. As shown above, this code ordinarily would fail to compile with an error saying that me could not destroyed because it was in a partially initialized state. But note that we’ve also added use of __disable_del at the end:

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

	fn __del__(owned self):
		print("deinitializing", self)

fn main():
	var me = Person("Connor", 25)

	var name = me.name^

	__disable_del me

If we execute this code, we see that, unlike before, the compiler accepts the code—but, additionally, the print from the __del__() statement is not executed, as it was in the previous example (the output is empty):

So what’s going on here, why does this work? The answer is that __disable_del is a low-level way to change the Mojo compiler’s logically initialized state associated (at compile time) with each in-scope object. This has the side effect of preventing the compiler from inserting a __del__() call.

Visualizing the code above, after we move out using me.name^, the object is in a logically initialized (but field-wise only partially initialized) state. But when the subsequent __disable_del operation is observed by the compiler, the object transitions into being in a logically deinitialized state:

When an object is in a logically deinitialized state, the compiler does not insert a __del__() call like it ordinarily would. Without the __del__() call, the earlier error saying that we’re “preventing the overall value from being destroyed” does not apply, and the program compiles successfully.

Forgetting Objects and Object Trees

I want to take a moment to more precisely characterize the effect __disable_del has — particularly in comparison to Rust’s mem::forget() function, with which __disable_del has an obvious comparison.

At first glance, __disable_del and mem::forget() seem similar: they both prevent each language’s equivalent of a destructor method (__del__() / Drop::drop()) from running. However, they differ in a subtle but important way:

__disable_del prevents a single object’s destructor from running
mem::forget() prevents an entire tree of object’s destructors from running

What does that mean in practice?

Let’s consider an example program where objects print a message whenever they’re destroyed. This should let us see the order in which objects are destroyed:

@fieldwise_init
struct Element(Copyable, Movable):
	var msg: String

	fn __del__(owned self):
		print("@ Element.__del__: destroying", self.msg)

@fieldwise_init
struct Container(Copyable, Movable):
	var a: Element
	var b: Element
	var c: Element

	fn __del__(owned self):
		print("@ Container.__del__")

fn main():
	var container = Container(Element("Foo"), Element("Bar"), Element("Baz"))
Both Element and Container print a message when they are destroyed:
In Mojo, objects are destroyed ‘as soon as possible’, aka. ASAP destruction, leading to print ordering shown above that is slightly different than equivalent code in C++ or Rust.

The basic principle behind ASAP destruction is that objects are destroyed immediately after their last use, even in the middle of an expression. This has several benefits.

To clarify what happens in the above example, let’s add some uses of the a, b, and c fields inside of Container.__del__:

@fieldwise_init
struct Element(Copyable, Movable):
	var msg: String

	fn __del__(owned self):
		print("@ Element.__del__: destroying", self.msg)

@fieldwise_init
struct Container(Copyable, Movable):
	var a: Element
	var b: Element
	var c: Element

	fn __del__(owned self):
		print("@ Container.__del__")
		use(self.c)
		use(self.b)
		use(self.a)

fn main():
	var container = Container(Element("Foo"), Element("Bar"), Element("Baz"))
The self.a, self.b, and self.c fields are dropped immediately after their last use—even though that order is different from the order the fields were originally declared in:

From the output, we can see that Container and all of its Element fields were destroyed, as expected.

Now let’s try this example again, but mark container using __disable_del:

# ...

fn main():
	var container = Container(Element("Foo"), Element("Bar"), Element("Baz"))

	__disable_del container

The difference from before is subtle, but important: Note that even though container.__del__() was never called, its fields were still destroyed.

(For clarity, this time the destruction of the Element fields happens after their last use inside the body of main(), since ownership was never passed to Container.__del__().)

That behavior might seem obvious, but for programmers coming to Mojo from Rust, it could be unexpected. Let’s see what the similar (but not equivalent) Rust code looks like, first without a mem::forget():

struct Element {
    msg: &'static str
}

impl Drop for Element {
    fn drop(&mut self) {
        println!("@ Element.drop: destroying {}", self.msg)
    }
}

struct Container {
    a: Element,
	b: Element,
	c: Element,
}

impl Drop for Container {
	fn drop(&mut self) {
		println!("@ Container.drop")
	}
}

fn main() {
    let container = Container {
		a: Element { msg: "Foo" },
		b: Element { msg: "Bar" },
		c: Element { msg: "Baz" },
	};
    
	// `container` gets dropped automatically at the end
	// of scope.
}

There is a slight difference in drop order (due to ASAP destruction), but as in the Mojo code, both the fields and the overall container have their destructors run. Now, without changing anything else, let’s try adding in a call to mem::forget():

// ...

fn main() {
    let container = Container {
		a: Element { msg: "Foo" },
		b: Element { msg: "Bar" },
		c: Element { msg: "Baz" },
	};
    
	std::mem::forget(container);
}

...and, nothing? By “forgetting” container, we don’t just prevent a single destructor from running, we prevent a “tree” of all objects “rooted” in the forgotten object — so the fields (and any sub-fields, or sub-sub-fields, etc. all the way down) are forgotten as well.

Let’s visualize the difference between these two behaviors, __disable_del and mem::forget():

In the case of __disable_del, only the top-level Container instance is “forgotten”. But with mem::forget(), every single value in the object tree is “forgotten”: The top level container, the a, b, and c fields, and the subfields a.msg, b.msg, and c.msg.

In this way, __disable_del is a much more narrowly targeted operation that mem::forget().

Lifecycle Method Special Behavior

Now that we’ve built up an understanding of some of the nuanced behavior of Mojo object initialization for a user of a type, we can take a closer look at what the situation looks like for a type author. In Mojo, there are four kinds of special methods that are called automatically by the compiler when certain “lifecycle” events happen to an object instance.

__init__(out self, ...) — called when an object is initialized
__del__(owned self) — called automatically when an object is no longer used and needs to be deinitialized
__moveinit__(out self, owned other: Self) — called to move an object from one location in memory to another
__copyinit__(out self, other: Self) — called when an object needs to be copied

Syntactically, these lifecycle methods mostly look like any other Mojo method. But, they each have certain special behavior that can’t trivially be replicated by other, general purpose, methods.

In this article, we’ll focus in on __init__() and __del__().

1. __init__(out self, ...)

In most methods, the out convention refers to an object that must be initialized before the function returns. But in __init__() methods, out applied to the self argument has slightly different behavior: it refers to an object that starts logically initialized, and merely needs to be fully initialized field-wise by assigning values to each field, in the function body.

In other words, within an __init__() method, the initialization status of the self object can be thought to look like this:

To illustrate, consider an __init__() method that is incomplete:

struct Person:
	var age: Int
	var height: Int

	fn __init__(out self, age: Int, height: Int):
		pass

The compiler identifiers that we’ve failed to initialize one or more of the fields of the object being constructed and errors:

To fully initialize the object, values must be assigned to each and every field, as we’ve already seen many examples of above.

The out argument convention is not specific to __init__() methods; any function can use out arguments.

The out argument convention is simply alternative syntax to using “arrow notation” (-> Foo) for specifying the function return type. However, out has the benefit of providing a named location to write the return value into, enabling ‘return value optimization’:

fn foo() -> String:
	return "hello"
	
fn bar(out x: String):
	x = "world"

# Both methods are called identically; the `out` argument is a normal return
# value to callers of `bar`:
fn main():
	print(foo(), bar())

The thing that makes the out argument convention special in __init__() is that field-wise initialization is not allowed except in __init__() methods:

struct Person:
	var name: String
	var age: Int

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

fn main():
	var _ = Person("Connor", 25)
Builds successfully:
struct Person:
	var name: String
	var age: Int

	@staticmethod
	fn custom_init(out self: Person, owned name: String, age: Int):
		self.name = name^
		self.age = age

fn main():
	var _ = Person.custom_init("Connor", 25)

Fails to build with an error, as field-wise initialization is only supported when self is considered logically initialized already:

The special treatment of out self in is the main way that __init__() functions are “magically” in a way that arbitrary functions in Mojo are not.

Other than writing to fields, what are other ways to interact with self inside of __init__() methods?

One thing we can try is calling methods. There are essentially two locations be might try a method call: before and after all of the fields have been initialized. Let’s compare:

It is not valid to call methods on objects with uninitialized fields, within __init__() methods or anywhere else (despite self being logically initialized within __init__ methods):

struct Person:
	var name: String
	var age: Int

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

		self.name = name^
		self.age = age

	fn greet(self):
		print("Hello", self.name)

fn main():
	var _ = Person("Jane", 25)

Since self is considered initialized within __init__() methods, after all of the fields are initialized, it is valid to call methods:

struct Person:
	var name: String
	var age: Int

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

		self.greet()

	fn greet(self):
		print("Hello", self.name)

fn main():
	var me = Person("Jane", 25)

2. __del__(owned self)

Within a __del__() method—and unlike in other owned self methods—the object is considered logically deinitialized at the end of the function, implicitly as if __disable_del self appeared at the end.

One way of validating that __del__() methods are special is that they are allowed to take ownership of field values. Consider this example, which does not compile:

struct Person:
	var name: String
	var foo: String

	fn consume(owned self):
		var _name = self.name^

If we merely rename consume to __del__, the exact same function body is no longer considered to be invalid:

struct Person:
	var name: String
	var age: Int

	fn __del__(owned self):
		var name = self.name^

fn main():
	pass

Conclusion & Related Reading

I wanted to write about object initialization in Mojo because I think details matter in programming language design, and Mojo is innovating in the area of object lifecycle in several small but meaningful ways, including:

Support for non-movable types, which are nice to use in practice due to features like out result syntax for initializing named return values, and will enable Mojo to define away challenging issues like those with self-referential types that are cleverly but clunkily worked-around by Pin in Rust.
ASAP destruction, which changes deinitializer ordering from what folks might expect, but avoids issues like scope-based destruction hampering tail call optimizations.

These nuanced design features and others help Mojo programmers develop abstractions that are fun to write, tightly specified, safe, and difficult to misuse, all while being maximally performant. That is not an easy set of constraints to manage, and is possible only by caring deeply about how features interact and compose together.

The following resources may be of interest to folks who want to learn more about the nuanced semantics of object initialization in Mojo and other programming languages:

The Mojo Manual: Intro to value lifecycle — practical introduction to the Mojo object lifecycle and how Mojo treats values more generally.

The Rustonomicon: Constructors — talks about ‘The One True Constructor’ syntax used in Rust. Interesting take on another way to model object construction.