Initial Reflections on Mojo v25.5.0
A funky assortment of novel technology that feels somewhat familiar! I'm eager to see how the Modular team is able to elevate the Python language and ecosystem. Given the modern MLIR technology stack it will be interesting to see the language morph over time. Mojo appears to give me more power with expressive features like traits and more responsibility with pointers plus an ownership model. Excited to build out some smaller projects and start benchmarking the language against various Python and PyTorch implementations!
Setup (Mojo v25.5.0.dev2025062815)
- Setup was really minimal only a few commands to get started though the docs currently want the nightly build installed and it was not clear why they did not want to install the stable build.
-
Steps:
-
Install package manager (pixi):
curl -fsSL https://pixi.sh/install.sh | sh -
Create project called 'life':
pixi init life \
-c https://conda.modular.com/max-nightly/ -c conda-forge && cd life -
Add modular (mojo included):
pixi add modular -
Verify modular installation:
pixi run mojo --version -
Project structure and launching the virtual environment shell:
-
Two files to note would be the toml and lock files that were created.
- A .toml for managing the projects dependencies.
- A lock for mananging all the transitive dependencies of the project.
- Similar to python conda, venv or uv workflows we will create and manage specific projects using virtual environments.
- A directory was created '.pixi' which contains a conda virtual env.
-
We need to launch a shell in this virtual env to run our mojo program.
Launch shell session in virtual env:
pixi shell
-
Two files to note would be the toml and lock files that were created.
-
Create a .mojo file with a main function and you can now run it. In the docs we'll be creating a life.mojo file and recreating Conway's Game of Life.
Run mojo program:
mojo life.mojo
-
Install package manager (pixi):
-
I had some issues setting up with pixi specifically due to the fact I'm using windows subsystem for linux.
When runnning the 'pixi shell' command I was seeing errors related to the new shell creation and the fact that my .bashrc file is leveraging 'sudo' commands on startup.
Running the command:
eval "$(pixi shell-hook)"allowed me to temporarily get around this.
Basic Syntax Notes
-
Can declare variables with and without a keyword. The following are synonymous:
var name: String = input("Who are you? ")name: String = input("Who are you? ")
-
Signed integers are represented with
Inttype. Representing CPU word size (32 or 64bit). Some functions/methods may returnInt64types, which are not the same asInt. To handle these cases we can pass in theInt64type to theInt()constructor to convert. -
Dynamic sequence/collection is represented with
Listtype and values must be the same type for type safety.- Integer list ->
row = List[Int]() - String list ->
row = List[String]() -
List initialization can be done two ways:
- List constructor ->
row = List(1, 2, 3) - List literal ->
row = [1, 2, 3]
- List constructor ->
-
Python List operations that are available:
row.append(), row.pop(), len(row)
- Integer list ->
-
Functions can be defined with
defandfn:-
Both definitions require you to declare the type of all parameters and arguments.
The core difference between the two is due to error handling.
We can operate from the basis that
fnprovides more fine grain control anddefprovides sensible defaults. Specificallyfncan only raise an error if theraisesdeclaration is used. - Similar to Python arguments can be optional and postional/keyword based. Where things feel like they begin to diverge are with Variadic arguments, which can be homogenous and heterogenous. Homogenous variadic args are similar to python *args (variable positional arguments) and **kwargs (variable keyword arguments). But Heterogenous variadic arguments allow for these passed in args to be of differing types and require traits which makes the syntax feel very foreign.
-
Here is an example function that we will be using to print the grid as a string:
def grid_str(rows: Int, cols: Int, grid: List[List[Int]]) -> String: str = String() for row in range(rows): for col in range(cols): if grid[row][col] == 1: str += "*" else: str += " " if row != rows - 1: str += "\n" return str -
While we are using range to iterate over the list Mojo has the ability to directly iterate over a collection using for without range plus list indexing.
nums = [1, 2, 3] for num in nums: print("Number: ", num)
-
Both definitions require you to declare the type of all parameters and arguments.
The core difference between the two is due to error handling.
We can operate from the basis that
-
Structs can be used to create custom types.
These are similar to classes, but do not support inheritance and Mojo current does not support classes for the time being.
Mojo structs have the following components:
- Fields: variables containing the data of the struct, which are accessed using dot notation.
-
Methods: optional functions that manipulate the data of the struct.
Read only methods need to be defined with keyword
selfas Mojo passes the instance as the first argument. For methods that mutate the original instance we need to usemut self. Mojo also has a@staticmethodannotation that allows us to define methods that are not attached to any struct instance.
__init__()method or we can use the@fieldwise_initdecorator to reduce boilerplate. -
Another Mojo divergence from Python is the introduction of Traits and how we can define these on the struct to control behavior of the struct lifecycle (create, move, copy, destroy).
Traits are effectively a set of methods that must be implemented for a given type. The following struct definition has a copyable and moveable trait.
@fieldwise_init struct Grid(Copyable, Movable): var rows: Int var cols: Int var data: List[List[Int]] -
Just like Python Mojo contains packages and modules, which are just mojo source files.
We can import from other modules following the familiar Python syntax:
from test_module import example_function.
Integrating Python Code in a Mojo Project
-
An exciting feature of Mojo is the ability to integrate Python code into a Mojo project, which allows us to leverage the vast Python ecosystem.
We need to add the Python dependency and associated Python packages we want to interact with to the
pixi.tomlfile that tracks the Mojo project dependencies. -
To do this we simply leverage pixi to update the virtual environment dependencies.
pixi add "python>=3.11,<3.13" "pygame>=2.6.1,<3"Note Mojo does not package a Python runtime when it builds a Mojo project meaning the environment executing the project code must include a valid Python version and associated dependencies. -
We can import a Python module using
Python.import_module(), which will return a reference type of 'PythonObject' that acts as a wrapper. From this point the python module can be used in the Mojo project.
Tutorial Code - Conway's Game of Life
gridv1.mojo:
import random
@fieldwise_init
struct Grid(Copyable, Movable, StringableRaising):
var rows: Int
var cols: Int
var data: List[List[Int]]
def __str__(self) -> String:
str = String()
for row in range(self.rows):
for col in range(self.cols):
if self[row, col] == 1:
str += "*"
else:
str += " "
if row != self.rows-1:
str += "\n"
return str
def __getitem__(self, row: Int, col: Int) -> Int:
return self.data[row][col]
def __setitem__(mut self, row: Int, col: Int, value: Int) -> None:
self.data[row][col] = value
@staticmethod
def random(rows: Int, cols: Int) -> Self:
random.seed() # seed based on current time -> value is always unique
var data: List[List[Int]] = []
# _ follows the discard pattern. Otherwise the mojo compiler would throw a warning.
for _ in range(rows):
var row_data: List[Int] = []
for _ in range(cols):
row_data.append(Int(random.random_si64(0, 1)))
data.append(row_data)
return Self(rows, cols, data)
def evolve(self) -> Self:
next_generation = List[List[Int]]()
for row in range(self.rows):
row_data = List[Int]()
# calc neighboring row indicies
row_above = (row - 1) % self.rows
row_below = (row + 1) % self.rows
for col in range(self.cols):
# calc neighboring col indicies
col_left = (col - 1) % self.cols
col_right = (col + 1) % self.cols
# determine the number of populated cells
num_neighbors = (
self[row_above, col_left]
+ self[row_above, col]
+ self[row_above, col_right]
+ self[row, col_left]
+ self[row, col_right]
+ self[row_below, col_left]
+ self[row_below, col]
+ self[row_below, col_right]
)
# determine the state of current cell for next generation
new_state = 0
if self[row, col] == 1 and (num_neighbors == 2 or num_neighbors == 3):
new_state = 1
elif self[row, col] == 0 and num_neighbors == 3:
new_state = 1
row_data.append(new_state)
next_generation.append(row_data)
return Self(self.rows, self.cols, next_generation)
life.mojo:
from gridv1 import Grid
from python import Python
import time
def run_display(
owned grid: Grid,
window_height: Int = 600,
window_width: Int = 600,
background_color: String = "black",
cell_color: String = "green",
pause: Float64 = 0.1,
) -> None:
pygame = Python.import_module("pygame")
pygame.init()
window = pygame.display.set_mode(
Python.tuple(window_height, window_width)
)
pygame.display.set_caption("Conway's Game of Life")
cell_height = window_height / grid.rows
cell_width = window_width / grid.cols
border_size = 1
cell_fill_color = pygame.Color(cell_color)
background_fill_color = pygame.Color(background_color)
running = True
while running:
event = pygame.event.poll()
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE or event.key == pygame.K_q:
running = False
window.fill(background_fill_color)
for row in range(grid.rows):
for col in range(grid.cols):
if grid[row, col]:
x = col * cell_width + border_size
y = row * cell_height + border_size
width = cell_width - border_size
height = cell_height - border_size
pygame.draw.rect(
window,
cell_fill_color,
Python.tuple(x, y, width, height),
)
pygame.display.flip()
time.sleep(pause)
grid = grid.evolve()
pygame.quit()
def main():
start = Grid.random(16,16)
run_display(start)