January 21, 2025

Hands-on with Mojo 24.6

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.

Key changes overview

One of the highlights of this release is the renaming of several core concepts to better reflect their purpose:

  • inoutmut 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)

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()

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

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

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 model

One 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

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)

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.

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()

The bootstrap_example method demonstrates how named results work:

  1. We declare the result parameter with out manager: TaskManager.
  2. We initialize and populate the manager inside the function.
  3. The return statement implicitly returns the named result.

Try this example:

Bash
magic run example5

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)

Let's try this example:

Bash
magic run example6

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 scope
  • Move semantics with the ^ operator
  • Non-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^

Run the complete last example with:

Bash
magic run example7

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

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
  • 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! 🔥

Ehsan M. Kermani
,
AI DevRel

Ehsan M. Kermani

AI DevRel

Ehsan is a Seasoned Machine Learning Engineer with a decade of experience and a rich background in Mathematics and Computer Science. His expertise lies in the development of cutting-edge Machine Learning and Deep Learning systems ranging from Natural Language Processing, Computer Vision, Generative AI and LLMs, Time Series Forecasting and Anomaly Detection while ensuring proper MLOps practices are in-place. Beyond his technical skills, he is very passionate about demystifying complex concepts by creating high-quality and engaging content. His goal is to empower and inspire the developer community through clear, accessible communication and innovative problem-solving. Ehsan lives in Vancouver, Canada.