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

Get started with Mojo

Get started with Mojo by building Conway's Game of Life, a simulation in which cells live, die, and reproduce based on the state of their neighbors.

This tutorial walks you through the steps to build a simple version of the game. It should take you about 45-60 minutes to complete. Don't feel you need to rush it.

Whether you arrive from, say, C++ or Python, you'll encounter unfamiliar syntax like transfer operators and compile-time variables. Read the Checkpoint items to map these new features to concepts you already know.

Run the code in your terminal and watch the grid evolve over time. As you build the game, you'll learn the Mojo syntax you need to start writing programs of your own.

Game state

Conway's Game of Life runs on a two-dimensional grid. Each cell is either alive or inactive. You'll use 1 for live cells and 0 for inactive cells. Count the neighbors around each cell by adding their values to a running sum.

Create life.mojo. Build an 8 x 8 grid containing a glider:

def main():
var count: Int = 64
var num_cols: Int = 8

var glider_grid = List[Int](length=count, fill=0)

# Convert (x, y) coordinates to a linear index in the grid
var to_index = lambda (x: Int, y: Int) -> Int: y * num_cols + x

# Set up the grid
for coord in [(0, 1), (1, 2), (2, 0), (2, 1), (2, 2)]:
glider_grid[to_index(coord[0], coord[1])] = 1

# Print the grid
for index in range(count):
print("X" if glider_grid[index] else ".", end="")
if index % num_cols == (num_cols - 1): print()

Run the program to see the initial glider configuration:

mojo life.mojo

Output:

..X.....
X.X.....
.XX.....
........
........
........
........
........

Checkpoint

  • All variable declarations like count and num_cols start with var.
  • var bindings are mutable by default.
  • The to_index lambda expression defines a small local function that converts coordinates to a list index. Lambdas are short anonymous functions that evaluate a single expression.

When assigning contents, a list expression sets the values:

var values: List[Int] = [12, -7, 64] # This is a list expression

In type names, square brackets supply compile-time parameters:

List[Int] # List is a standard library-supplied type
Grid[8, 8] # Grid is a custom type

Parentheses supply run-time arguments:

print(value)
Grid[8, 8]()

Add reusable printing

Move the display loop to a reusable function. Place this above main():

def print_grid(grid: List[Int], num_cols: Int):
for index in range(len(grid)):
print("X" if grid[index] else ".", end="")
if index % num_cols == (num_cols - 1): print()

Replace the print loop with print_grid(glider_grid, num_cols) and run.

Improve performance by replacing individual prints with a single string:

def print_grid(grid: List[Int], num_cols: Int):
var grid_str = ""
for index in range(len(grid)):
grid_str += "X" if grid[index] else "."
if (index % num_cols == (num_cols - 1) and
index != len(grid) - 1):
grid_str += "\n"
print(grid_str)

When running the program with larger grids and many generations, this approach is faster than printing each cell individually. You print once per frame instead of once per cell.

Call print_grid() from main() and confirm the output:

# Print the grid
print_grid(glider_grid, num_cols)

Add lookups

Make a few more changes in place in life.mojo:

comptime count: Int = 64
comptime num_cols: Int = 8

# Transform coordinates to a linear index
comptime to_index = lambda (x: Int, y: Int) -> Int: (
y * num_cols + x
)

# Transform linear index to coordinates
comptime to_coord = lambda (i: Int) -> Tuple[Int, Int]: (
(i % num_cols, i // num_cols) # `//` is flooring divide
)

def print_grid(grid: List[Int]):
for index in range(len(grid)):
print("X" if grid[index] else ".", end="")
if index % num_cols == (num_cols - 1): print()

def main():
var glider_grid: List[Int] = List[Int](length=count, fill=0)

# Set up the grid
for coord in [(0, 1), (1, 2), (2, 0), (2, 1), (2, 2)]:
glider_grid[to_index(coord[0], coord[1])] = 1

# Print the grid
print_grid(glider_grid)

Now you can convert coordinates to indices and indices to coordinates.

Checkpoint

  • comptime declarations are evaluated at compile time before the code runs. This lets Mojo optimize and generate efficient machine code.
  • The flooring divide operator (//) used here performs integer division, rounding towards negative infinity. For integer types, / returns an integer, rounding towards zero.
  • You've moved all the constants out from main() to comptime declarations, making them available throughout this file.
  • You've created two lambda expressions that convert between coordinates and indices.
  • You've removed the second argument from print_grid().

Try this

In a separate source file, declare the number of rows as a comptime constant and compute the count at compile time.

Remove the new source after verifying your solution is correct. You will always need two of these three items: the number of rows, the number of columns, and the total count of cells.

Define a Grid type

Revise your code again, into a new file called grid.mojo. You're creating a new type, called a struct:

struct Grid[num_cols: Int, num_rows: Int]:
var cells: List[Int]
var count: Int

def __init__(out self):
self.count = Self.num_cols * Self.num_rows
self.cells = List[Int](length=self.count, fill=0)

def main():
var glider_grid = Grid[8, 8]()

__init__(out self) initializes a new Grid. Every field must receive a value before the initializer returns.

Here, count comes from the grid dimensions, and cells starts as a list of zeros.

Checkpoint

  • Grid uses both compile-time parameters and runtime fields.

  • Self.num_cols and Self.num_rows belong to the parameterized type.

  • self.cells and self.count belong to one Grid instance.

  • This Grid needs custom initialization, so it defines __init__(). Normally, if an initializer just assigns arguments directly to fields, add @fieldwise_init to the struct instead. Mojo generates that initializer for you. For example, in an alternate implementation you might define all three core measurements as fields in your struct rather than synthesize one from the other two:

    @fieldwise_init
    struct Grid:
    var cells: List[Int]
    var count: Int
    var num_cols: Int
    var num_rows: Int

Add grid operations

Put the following content into your Grid struct:

comptime to_index = lambda (x: Int, y: Int) -> Int: (
y * Self.num_cols + x
)

comptime to_coord = lambda (i: Int) -> Tuple[Int, Int]: (
(i % Self.num_cols, i // Self.num_cols)
)

def print_grid(self):
var grid_str = ""
for index in range(len(self.cells)):
grid_str += "X" if self.cells[index] else "."
if (index % Self.num_cols == (Self.num_cols - 1) and
index != len(self.cells) - 1):
grid_str += "\n"
print(grid_str)

def __setitem__(mut self, coord: Tuple[Int, Int], value: Int):
self.cells[Self.to_index(coord[0], coord[1])] = value

def __getitem__(self, coord: Tuple[Int, Int]) -> Int:
return self.cells[Self.to_index(coord[0], coord[1])]

Checkpoint

  • Self.to_index() accesses a type member. The lambda to_index() belongs to Grid. Uppercase Self refers to the type.
  • Instance methods take self as their first argument. Lowercase self refers to an instance.
  • mut self allows a method to change the instance.
  • __getitem__() and __setitem__() define indexing behavior, so a Grid can use grid[coord] notation.

Don't add this anywhere. It's a preview of how this all works from the call site:

# Set up the grid
for coord in [(0, 1), (1, 2), (2, 0), (2, 1), (2, 2)]:
glider_grid[coord] = 1 # Uses indexing with `__setitem__()`

Import Grid

Remove main() from grid.mojo, save it, and import your new type into life.mojo:

from grid import Grid # Separate concerns into separate files

def main():
var glider_grid = Grid[8, 8]()

# Set up the grid
for coord in [(0, 1), (1, 2), (2, 0), (2, 1), (2, 2)]:
glider_grid[coord] = 1

# Print the grid
glider_grid.print_grid()

Run it and confirm everything works as expected.

Be random

Gliders are terrific for validating code, but random values produce great animations. Set up your seed in main():

from std.random import seed
from grid import Grid

def main():
seed()

# ...

Mojo won't allow runtime statements at global scope, so you can't call seed() there. Instead, seed the RNG from executable code, such as main() or another function. You only need to seed once: seeding sets the state of a single PRNG shared across threads.

Add protection

Filling every cell independently gives you random static. It works, but it doesn't produce especially interesting Game of Life patterns. Instead, build a few random clumps of live cells.

To start, add checks to your setter in grid.mojo. A coordinate check function helps:

def is_valid_coord(self, coord: Tuple[Int, Int]) -> Bool:
return (
not (
coord[0] < 0
or coord[0] >= Self.num_cols
or coord[1] < 0
or coord[1] >= Self.num_rows
)
)

def __setitem__(mut self, coord: Tuple[Int, Int], value: Int):
if not self.is_valid_coord(coord): return # no op
self.cells[Self.to_index(coord[0], coord[1])] = value

You can add a check to the getter, too. Instead of returning a made-up value, raise an error or abort the process. The best approach tests the coordinates before indexing (if self.is_valid_coord(coord):) to avoid indexing errors instead of adding no-op workarounds.

Introducing errors

This version of __getitem__() raises an error when coordinates aren't valid. Add raises to the signature before the arrow, and call raise:

def __getitem__(self, coord: Tuple[Int, Int]) raises -> Int:
if not self.is_valid_coord(coord):
raise String(t"Invalid coordinate: ({coord[0]}, {coord[1]})")

return self.cells[Self.to_index(coord[0], coord[1])]

A TString starts with t" and creates a template format. print() statements automatically convert to strings, but everywhere else explicitly call String().

Once __getitem__() raises, every caller must either raise or handle the error using Mojo's try/except error handling.

For now, revert your changes and let __getitem__() handle invalid coordinates as it did before.

Construct the random grid

Add a static method that constructs a random Grid:

from std.random import random_si64 # Add this import to grid.mojo

# and inside Grid:

@staticmethod
def random_grid(clumps: Int = 2) -> Self:
var grid = Self()
for _ in range(clumps):
var idx = Int(random_si64(0, Int64(grid.count) - 1))
var x, y = Self.to_coord(idx)
grid[(x, y)] = 1 # Fill index cell

for dx in range(-1, 2): # -1, 0, or 1
for dy in range(-1, 2):
if not grid.is_valid_coord((x + dx, y + dy)): continue
if random_si64(0, 3) > 0: continue # 75% skip
grid[(x + dx, y + dy)] = 1
return grid^

By default, random_grid() builds two clumps when called without arguments (clumps: Int = 2). The default value follows the equal sign.

A @staticmethod belongs to the type and not an individual instance. Call it through the Grid type:

print("\nRandom grid:\n")
var random_grid = Grid[8, 8].random_grid()
random_grid.print_grid()

You don't have to add this code except to test it. You can remove it afterwards.

Checkpoint

  • This static method still needs Grid parameters, namely 8 and 8.
  • Each neighboring cell has a 25% chance of becoming live. Adjust to your preference.
  • random_si64() returns an Int64. Cast the value to Int.
  • The clump loop variable is _, the discard pattern. Use this when you don't care about the value it produces.
  • A ^ sigil transfers the newly initialized Grid, avoiding a copy. Transfer means changing ownership, handing the value to the new owner. By skipping a copy, you avoid the time and memory costs of duplicating data.

Evolve

In life.mojo, cut out the glider grid and remove the print statements. Next, you'll start making the grid change over time. Start by adding a separate next_cells list to Grid and initialize it:

struct Grid[num_cols: Int, num_rows: Int]:
var cells: List[Int] # Holds the current generation
var next_cells: List[Int] # Holds the next generation
var count: Int

def __init__(out self):
self.count = Self.num_cols * Self.num_rows
self.cells = List[Int](length=self.count, fill=0)
self.next_cells = List[Int](length=self.count, fill=0)

Conway's Game of Life applies three rules to every cell:

  • A live cell stays alive with two or three live neighbors.
  • An inactive cell becomes alive with exactly three live neighbors.
  • Every other cell is inactive in the next generation.

The updated cells are stored in the next_cells list. They won't affect math for the previous generation.

Each cell has eight neighbors: the cells in the 3 x 3 square around it, excluding the cell itself.

Add these methods to Grid.

evolve_cell() determines the state for the next generation of a single cell based on its neighbors:

def evolve_cell(mut self, i: Int):
var is_live = Bool(self.cells[i])
self.next_cells[i] = 0

# Count the neighbors
var ncount = -1 if is_live else 0 # Exclude self from the count
var x, y = Self.to_coord(i)
for dx in range(-1, 2):
for dy in range(-1, 2):
var nx = x + dx
var ny = y + dy
ncount += self.cells[Self.to_index(nx, ny)]

# Live cell stays alive with two or three live neighbors
if is_live and (ncount == 2 or ncount == 3):
self.next_cells[i] = 1
# Inactive cell becomes alive with exactly three live neighbors
elif not is_live and ncount == 3:
self.next_cells[i] = 1

evolve() updates the entire grid to the next generation by calling evolve_cell() for each index:

def evolve(mut self):
for i in range(self.count):
var x, y = Self.to_coord(i)
# Edges are excluded from evolution and will always go inactive.
if (x == 0 or y == 0 or
x == Self.num_cols - 1 or y == Self.num_rows - 1):
self.next_cells[i] = 0 # Edges go inactive
continue
self.evolve_cell(i)

# Swap the current and next cell states
var tmp = self.cells^ # Transfer
self.cells = self.next_cells^ # Transfer
self.next_cells = tmp^ # Every cell is written, so this is safe

Checkpoint

  • The algorithm progresses in integer order, excluding edges.
  • Live cells offset by -1, to exclude them from the count.
  • Mojo's "ternary" has no ? : syntax. Use the Python-style if-else expression instead: -1 if is_live else 0.

Run the simulation

Add a loop so you can watch the grid evolve in the terminal. Here's the final life.mojo:

from std.random import seed
from grid import Grid
from std.time import sleep

def main():
comptime gridw: Int = 80
comptime gridh: Int = 20
comptime grid_count: Int = 400

seed()
var grid = Grid[gridw, gridh].random_grid(grid_count)

while True:
for gen in range(100):
print(t"\033[H\033[J\nGeneration: {gen}{' ' * 4}")
grid.evolve(); grid.print_grid()
sleep(0.1)
grid = Grid[gridw, gridh].random_grid(grid_count)

Checkpoint

  • comptime declarations let you set constants.
  • The odd characters in the print statement are ANSI escape codes. You'll see the updates generation-by-generation. Keep your terminal at a minimum of 80x24 for best results.
  • Mojo uses semicolons to separate statements, not to end them. One statement per line is the usual form. This listing pairs them to stay compact.

Your first day? Try these

  • To use AI coding assistants with Mojo, see our AI skills guide for using the latest up-to-date language know-how.
  • Our Mojo language reference section provides a concise reference for syntax, keywords, and more.
  • You can download our cheat sheets for printable reference cards that unify entire concepts.
  • Mojo Quest is a web-based game where you solve coding challenges to practice Mojo syntax.

Final code

View the complete grid.mojo
from std.random import random_si64


struct Grid[num_cols: Int, num_rows: Int]:
var cells: List[Int] # Holds the current generation
var next_cells: List[Int] # Holds the next generation
var count: Int

def __init__(out self):
self.count = Self.num_cols * Self.num_rows
self.cells = List[Int](length=self.count, fill=0)
self.next_cells = List[Int](length=self.count, fill=0)

comptime to_index = lambda (x: Int, y: Int) -> Int: (
y * Self.num_cols + x
)

comptime to_coord = lambda (i: Int) -> Tuple[Int, Int]: (
(i % Self.num_cols, i // Self.num_cols)
)

def print_grid(self):
var grid_str = ""
for index in range(len(self.cells)):
grid_str += "X" if self.cells[index] else "."
if (
index % Self.num_cols == (Self.num_cols - 1)
and index != len(self.cells) - 1
):
grid_str += "\n"
print(grid_str)

def is_valid_coord(self, coord: Tuple[Int, Int]) -> Bool:
return not (
coord[0] < 0
or coord[0] >= Self.num_cols
or coord[1] < 0
or coord[1] >= Self.num_rows
)

def __setitem__(mut self, coord: Tuple[Int, Int], value: Int):
if not self.is_valid_coord(coord):
return # no op
self.cells[Self.to_index(coord[0], coord[1])] = value

def __getitem__(self, coord: Tuple[Int, Int]) -> Int:
return self.cells[Self.to_index(coord[0], coord[1])]

@staticmethod
def random_grid(clumps: Int = 2) -> Self:
var grid = Self()
for _ in range(clumps):
var idx = Int(random_si64(0, Int64(grid.count) - 1))
var x, y = Self.to_coord(idx)
grid[(x, y)] = 1 # Fill index cell

for dx in range(-1, 2): # -1, 0, or 1
for dy in range(-1, 2):
if not grid.is_valid_coord((x + dx, y + dy)):
continue
if random_si64(0, 3) > 0:
continue # 75% skip
grid[(x + dx, y + dy)] = 1
return grid^

def evolve_cell(mut self, i: Int):
var is_live = Bool(self.cells[i])
self.next_cells[i] = 0

# Count the neighbors
var ncount = -1 if is_live else 0 # Exclude self from the count
var x, y = Self.to_coord(i)
for dx in range(-1, 2):
for dy in range(-1, 2):
var nx = x + dx
var ny = y + dy
ncount += self.cells[Self.to_index(nx, ny)]

# Live cell stays alive with two or three live neighbors
if is_live and (ncount == 2 or ncount == 3):
self.next_cells[i] = 1
# Inactive cell becomes alive with exactly three live neighbors
elif not is_live and ncount == 3:
self.next_cells[i] = 1

def evolve(mut self):
for i in range(self.count):
var x, y = Self.to_coord(i)
# Edges are excluded from evolution and will always go inactive.
if (
x == 0
or y == 0
or x == Self.num_cols - 1
or y == Self.num_rows - 1
):
self.next_cells[i] = 0 # Edges go inactive
continue
self.evolve_cell(i)

# Swap the current and next cell states
var tmp = self.cells^ # Transfer
self.cells = self.next_cells^ # Transfer
self.next_cells = tmp^ # Every cell is written, so this is safe
View the complete life.mojo
from std.random import seed
from grid import Grid
from std.time import sleep


def main():
comptime gridw: Int = 80
comptime gridh: Int = 20
comptime grid_count: Int = 400

seed()
var grid = Grid[gridw, gridh].random_grid(grid_count)

while True:
for gen in range(100):
print(t"\033[H\033[J\nGeneration: {gen}{' ' * 4}")
grid.evolve()
grid.print_grid()
sleep(0.1)
grid = Grid[gridw, gridh].random_grid(grid_count)