IMPORTANT: To view this page as Markdown, append `.md` to the URL (e.g. /docs/manual/basics.md). For the complete Mojo documentation index, see llms.txt.
Skip to main content
Version: Nightly
For the complete Mojo documentation index, see llms.txt. Markdown versions of all pages are available by appending .md to any URL (e.g. /docs/manual/basics.md).

Explicit destruction

Explicit destruction prevents ASAP destruction from destroying a value at its last use. To opt-in, conform the type to an unsatisfiable Deinitable constraint:

struct Example(Deinitable where False):
def cleanup(deinit self):
pass

This disables automatic destruction via the __deinit__() method and requires explicit cleanup through named deinitializer methods. When using this pattern, you must call a named deinitializer method to consume the value. If you don't, the compiler emits an error.

Use explicit destruction when cleanup must be performed deliberately, may fail and require error handling, or your type offers multiple valid ways to end a value's lifetime (such as saving to file and closing the file descriptor, or quitting without saving).

Implicit vs. explicit destruction

Mojo supports two destruction models. Most types rely on compiler-managed lifetime analysis, while some types opt into explicit control for stronger cleanup guarantees.

Implicit destruction (the default): The compiler automatically calls __deinit__() when a value has no further uses. Cleanup is triggered by Mojo's lifetime analysis and requires no manual intervention. You may override __deinit__() in your type, but the compiler does not verify that cleanup is performed intentionally.

Explicit destruction: You intentionally call named deinitializer methods (such as cleanup() or save_and_close()). The conformance disables automatic destruction and the compiler enforces explicit consumption. Failing to call a deinitializer before a value leaves scope results in a compile-time error.

Most types use implicit destruction. Explicit destruction adds a layer of safety when cleanup may fail, requires error handling, or benefits from deliberate control over how a value's lifetime ends.

Basic usage

To opt into explicit, compiler-enforced destruction, mark types with Deinitable where False.

Provide named deinitializer methods that use the deinit self argument convention:

struct FileBuffer(Deinitable where False):
var path: String
var data: String

def __init__(out self, path: String):
self.path = path
self.data = ""

def write(mut self, content: String):
self.data += content

def save_and_close(deinit self) raises:
write_to_disk(self.path, self.data)

Declaring types with Deinitable where False requires you to add intentional calls to type-specific deinitializer methods before the end of scope or to transfer the value out of scope so it can be consumed at a later time:

def write_log(path: String, message: String) raises:
var buffer = FileBuffer(path)
buffer.write(message)
buffer^.save_and_close() # Required before `buffer` leaves scope

If you omit this call, the compiler emits an error.

Custom error messages

To improve compiler diagnostics for explicit destruction, include a custom error message with where constraint:

struct CustomFileHandle(
Deinitable where(False, "Must call save_and_close() or discard()")
):
# ... other implementation details ...

def save_and_close(deinit self) raises:
write_to_disk(self.path, self.data) # Store data, then end
self.close_descriptor()

def discard(deinit self):
pass # Abandon without writing
  • Death of a value - Complete coverage of value destruction and lifetime management
  • AnyType - Base trait for all types
  • Deinitable - Trait for automatically deinitializable types