























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
or update it via
Bash
magic self-update
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.
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.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.
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:
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)
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()
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
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
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")
Without the @implicit decorator, you would need to explicitly create Task objects:
Mojo
# Explicit conversion required manager.add_task(Task("Walk the dog"))
This new approach strikes a balance between convenience and type safety by making implicit conversions opt-in rather than automatic.
Bash
magic run example2
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
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
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)
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
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))
would produce the compiler error:
Output
# error: argument of 'pick_longer' call allows writing # a memory location previously writable through another aliased argument
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.
outMojo 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()
The bootstrap_example method demonstrates how named results work:
out manager: TaskManager.return statement implicitly returns the named result.Try this example:
Bash
magic run example5
Mojo 24.6 introduces two important additions to the standard library: Deque and OwnedPointer. Let's explore each one:
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)
Let's try this example:
Bash
magic run example6
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:
^ operatorHere'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^
Run the complete last example with:
Bash
magic run example7
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
This example illustrates:
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.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.out: Simplifying function return conventions for better readability and consistency.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.
Bash
b main b my_module::main
VS Code Extension Enhancements
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.
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! 🔥
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。