Mojo 24.6 has arrived with significant changes to argument conventions and lifetime management. This release marks an important step in Mojo's evolution, making its memory and ownership model more intuitive while maintaining strong safety guarantees. The changes are available now and are bundled with the MAX 24.6 release .
To proceed, make sure you have installed the magic
CLI
Bash
curl -ssL https://magic.modular.com/ | bash
Copy
or update it via
Bash
magic self-update
Copy
In this blog post, we'll explore these changes through practical examples that demonstrate the new syntax and features. We'll start with basic argument conventions and gradually introduce more advanced concepts like origins (formerly lifetimes) and implicit conversions . By the end, you'll have a thorough understanding of how to use these enhancements in your Mojo code.
One of the biggest highlights of this release is the significant contributions from our community. We received numerous pull requests from 11 community contributors that included new features, bug fixes, documentation enhancements, and code refactoring.
Special thanks ❤️ to our community contributors: @jjvraw, @artemiogr97, @martinvuyk, @jayzhan211, @bgreni, @mzaks, @msaelices, @rd4com, @jiex-liu, @kszucs, @thatstoasty For a complete list of changes, please refer to the changelog for version 24.6 .
All the code for this blog post is available in our GitHub repository .
Key changes overview One of the highlights of this release is the renaming of several core concepts to better reflect their purpose:
inout
→ mut
for mutable arguments: This better reflects that these parameters can be modified."lifetime" → "origin" for reference tracking: Better describes the concept of where references come from.These naming changes make Mojo code more intuitive while preserving the strong safety guarantees that developers expect. Let's dive into each feature with practical examples.
New argument conventions The most visible change in Mojo 24.6 is the renaming of argument conventions. The inout
keyword has been replaced with mut
. This change makes the code's intent clearer — mut
explicitly indicates that an argument can be modified.
Let's look at a practical example:
Before 24.6
Mojo
@value
struct Task:
var description: String
fn __init__(inout self, desc: String):
self.description = desc
struct TaskManager:
var tasks: List[Task]
fn __init__(inout self):
self.tasks = List[Task]()
fn add_task(inout self, task: Task):
self.tasks.append(task)
fn show_tasks(self):
for t in self.tasks:
print("- ", t[].description)
Copy
After 24.6
Mojo
@value
struct Task:
var description: String
# Notice: `inout` -> `out`
fn __init__(out self, desc: String):
# 'out self' indicates we are constructing a fresh Task
self.description = desc
struct TaskManager:
var tasks: List[Task]
fn __init__(out self):
# Another 'out self' constructor,
# sets up an empty list of tasks
self.tasks = List[Task]()
# Notice: `inout` -> `mut`
fn add_task(mut self, task: Task):
self.tasks.append(task)
fn show_tasks(self):
for t in self.tasks:
print("- ", t[].description)
def main():
# Create a new TaskManager
manager = TaskManager()
# Add tasks
manager.add_task(Task("Walk the dog"))
manager.add_task(Task("Write Mojo 24.6 blog post"))
manager.show_tasks()
Copy
To run the example with the magic
CLI:
Bash
git clone https://github.com/modular/devrel-extras
cd devrel-extras/blogs/hands-on-with-mojo-24-6
magic run example1
Copy
Implicit conversions Another important change in 24.6 is how implicit conversions are handled. Single-argument constructors now require the @implicit
decorator to allow implicit conversions. This makes type conversions more explicit and safer by requiring developers to opt-in to implicit conversion behavior.
Here's how it works:
Mojo
struct Task:
var description: String
@implicit # Explicitly opt-in to implicit conversion
fn __init__(out self, desc: String):
self.description = desc
@implicit # Also allow conversion from string literals
fn __init__(out self, desc: StringLiteral):
self.description = desc
Copy
This change allows for more convenient usage while maintaining type safety:
Mojo
def main():
manager = TaskManager()
# These now work because we opted into implicit conversion
# String → Task
manager.add_task(String("Walk the dog"))
# StringLiteral → Task
manager.add_task("Write Mojo 24.6 blog post")
Copy
Without the @implicit
decorator, you would need to explicitly create Task
objects:
Mojo
# Explicit conversion required
manager.add_task(Task("Walk the dog"))
Copy
This new approach strikes a balance between convenience and type safety by making implicit conversions opt-in rather than automatic.
Bash
magic run example2
Copy
Origins
: A more intuitive reference modelOne of the most significant changes in Mojo 24.6 is renaming "lifetimes" to "origins" and builds out the reference model significantly. This change better reflects what these annotations actually do—tracking where references come from rather than their complete lifecycle. Don’t forget to check the Mojo manual .
Let's explore this concept with some practical examples:
Mojo
struct TaskManager:
var tasks: List[Task]
fn get_task(ref self, index: Int) -> ref [self.tasks] Task:
# The [self.tasks] annotation shows
# this reference originates from the tasks list
return self.tasks[index]
def main():
manager = TaskManager()
manager.add_task("Initial task")
# Get a reference to the first task
first_task = manager.get_task(0)
first_task.description = "Modified task" # Safe modification
Copy
The origin annotation [self.tasks]
clearly indicates that the returned reference comes from the tasks list. This makes it easier to understand and track reference relationships in your code.
Bash
magic run example3
Copy
Working with multiple origins Origins become particularly powerful when working with multiple references:
Mojo
fn pick_longer(ref t1: Task, ref t2: Task) -> ref [t1, t2] Task:
# The [t1, t2] annotation shows this reference
# could come from either t1 or t2
return t1 if len(t1.description) >= len(t2.description) else t2
def main():
manager = TaskManager()
manager.add_task("Short task")
manager.add_task("This is a longer task")
# copies so a new origin is detected
first_task = manager.get_task(0)
first_task.description = "Walk the dog ASAP and then write the blog post!"
# Compare tasks by length
longer = pick_longer(first_task, manager.get_task(1))
print("Longer task: ", longer.description)
Copy
This function demonstrates how ref [t1, t2]
allows the returned reference to originate from either t1
or t2
, enabling more flexible and expressive reference management.
Run the example:
Bash
magic run example4
Copy
Note: Mojo 24.6 enforces strict argument exclusivity at compile time . For example, the following call:
Mojo
pick_longer(manager.get_task(0), manager.get_task(1))
Copy
would produce the compiler error:
Output
# error: argument of 'pick_longer' call allows writing
# a memory location previously writable through another aliased argument
Copy
This error occurs because the function attempts to modify a memory location that is already writable through another reference, ensuring memory safety and preventing potential data races.
Named results with out
Mojo 24.6 introduces a simpler syntax for named results by using the out
convention directly. This replaces the previous Type as out
syntax, making it more consistent with how we use out
elsewhere in the language.
Let's look at an example that demonstrates this new syntax:
Mojo
struct TaskManager:
var tasks: List[Task]
fn __init__(out self):
self.tasks = List[Task]()
# Named result using the new 'out' convention
@staticmethod
fn bootstrap_example(out manager: TaskManager):
manager = TaskManager()
manager.add_task("Default Task #0")
manager.add_task("Default Task #1")
return # 'manager' is implicitly returned
def main():
# Create a TaskManager with default tasks
mgr = TaskManager.bootstrap_example()
mgr.show_tasks()
Copy
The bootstrap_example
method demonstrates how named results work:
We declare the result parameter with out manager: TaskManager
. We initialize and populate the manager inside the function. The return
statement implicitly returns the named result. Try this example:
Bash
magic run example5
Copy
New collection types Mojo 24.6 introduces two important additions to the standard library: Deque
and OwnedPointer
. Let's explore each one:
Deque: Double-ended queue The new Deque
collection type provides efficient O(1) operations at both ends of the sequence. This is particularly useful when you need to add or remove elements from either end of a collection, such as implementing a work queue with priorities.
Mojo
from collections import Deque
struct TaskManager:
# Using Deque instead of List
var tasks: Deque[Task]
fn __init__(out self):
self.tasks = Deque[Task]()
fn add_task(mut self, task: Task):
# Add to back (normal priority)
self.tasks.append(task)
fn add_urgent_task(mut self, task: Task):
# Add to front (high priority)
self.tasks.appendleft(task)
Copy
Let's try this example:
Bash
magic run example6
Copy
OwnedPointer: Safe memory management The OwnedPointer
type provides safe , single-owner, non-nullable smart pointer functionality. This is particularly useful when dealing with resources that need deterministic cleanup, such as file handles or network connections.
Key features of OwnedPointer
:
Single ownership semantics Automatic cleanup when going out of scopeMove semantics with the ^
operatorNon-nullable (always points to valid data)Here's a basic example showing the concept:
Mojo
from memory import OwnedPointer
@value
struct HeavyResource:
var data: String
fn __init__(out self, data: String):
self.data = data
fn do_work(self):
print("Processing:", self.data)
struct Task:
var description: String
var heavy_resource: OwnedPointer[HeavyResource]
# We keep the @implicit for description
@implicit
fn __init__(out self, desc: StringLiteral):
self.description = desc
self.heavy_resource = OwnedPointer[HeavyResource](HeavyResource("Heavy resource with description: " + desc))
fn __moveinit__(out self, owned other: Task):
self.description = other.description^
self.resource = other.resource^
Copy
Run the complete last example with:
Bash
magic run example7
Copy
Putting It all together Let's look at a complete example that combines all these new features — argument conventions, origins, and collection types. This example demonstrates how the various improvements in Mojo 24.6 work together to create cleaner, safer, and more efficient code:
Mojo
from collections import Deque
from memory import OwnedPointer
from os import abort
@value
struct HeavyResource:
var data: String
fn __init__(out self, data: String):
self.data = data
fn do_work(self):
print("Heavy work:", self.data)
struct Task:
var description: String
var heavy_resource: OwnedPointer[HeavyResource]
# We keep the @implicit for description
@implicit
fn __init__(out self, desc: StringLiteral):
self.description = desc
self.heavy_resource = OwnedPointer[HeavyResource](HeavyResource("Heavy resource with description: " + desc))
fn __moveinit__(out self, owned other: Task):
self.description = other.description^
self.heavy_resource = other.heavy_resource^
# Workaround `CollectionElement` requirement for `Deque`
fn __copyinit__(out self, other: Task):
abort("__copyinit__ should never be called")
while True:
pass
fn do_work(self):
self.heavy_resource[].do_work()
struct TaskManager:
var tasks: Deque[Task]
fn __init__(out self):
self.tasks = Deque[Task]()
# Need to use `owned` to avoid Copy
fn add_task(mut self, owned task: Task):
self.tasks.append(task^)
fn add_urgent_task(mut self, owned task: Task):
# or to the front
self.tasks.appendleft(task)
fn show_tasks(self):
for t in self.tasks:
print("- ", t[].description)
@staticmethod
fn bootstrap_example(out manager: TaskManager):
manager = TaskManager()
manager.add_task("Deque-based Task #1")
manager.add_urgent_task("Deque-based Task #0")
return
fn do_work(owned self):
for t in self.tasks:
t[].do_work()
def main():
mgr = TaskManager.bootstrap_example()
print("Tasks:")
mgr.show_tasks()
print("Do work:")
mgr^.do_work()
# When 'mgr' goes out of scope, OwnedPointer cleans up all HeavyResources
# so no need for explicit cleanup
Copy
This example illustrates:
New out
, mut
, and read
Argument Conventions: Clear indications of how each method interacts with the struct's state.@implicit
Single-Argument Constructor Conversions: Conveniently adding tasks using string literals without explicit Task
construction.Origins with ref [a, b]
: Managing references that can originate from multiple sources, enhancing flexibility.Deque
Collection Type: Efficiently managing tasks with operations on both ends.OwnedPointer
for Safe Resource Management: Ensuring heavy resources are properly managed without manual cleanup.Named Results with out
: Simplifying function return conventions for better readability and consistency.Debugging with Mojo LLDB Debugger and VS Code Enhancements Mojo 24.6 also brings significant improvements to the tooling ecosystem , enhancing the developer experience with better debugging capabilities and editor integrations. In that regard,
mojo debug --rpc
has changed to mojo debug --vscode
. Please make sure to check the tooling section of the changelog for version 24.6 .
Mojo LLDB Debugger Enhancements Symbol Breakpoints: You can now set breakpoints on specific symbols within your code. For example:
Bash
b main
b my_module::main
Copy
Improved Error Messages: Error messages are now clearer and more concise, eliminating unnecessary type expansions and focusing on the core issue.VS Code Extension Enhancements
Data Breakpoints: Watch specific variables or struct fields for changes during execution.Function Breakpoints: Break execution when specific functions are called.Run and Debug Tab Automation: The extension now automatically opens the Run and Debug tab when a debug session starts.Enhanced LSP and Documentation Display: Origins and parametric types are displayed more clearly in language server protocols and generated API documentation.Conclusion Mojo 24.6 significantly enhances the language’s ergonomics while retaining its strong safety guarantees. The new argument conventions (mut
, out
) clarify code intent, and renaming "lifetimes" to "origins" better conveys their purpose. The introduction of Deque
and OwnedPointer
broadens the standard library’s capabilities, and the opt-in implicit conversion rules reinforce type safety in your workflows.
These updates make Mojo more intuitive and productive , all while preserving the language’s commitment to performance and safety. Whether you’re new to Mojo or an experienced developer, these improvements provide a more refined and powerful programming experience.
To try out these new features, update to Mojo 24.6 and check out the full changelog .
What’s next? Now that you've learned about the latest features in Mojo 24.6, it's time to put this knowledge into practice and dive deeper into the Mojo ecosystem. Here are some resources and next steps to help you get started and stay connected:
Until next time! 🔥