STOP: Each chapter of Programming for Lovers comprises two parts. First, the “core text” presents critical concepts at a high level, avoiding language-specific details. The core text is followed by “code alongs,” where you will apply what you have learned while learning the specifics of the language syntax. We strongly suggest starting with this chapter’s core text (click the button below) to grasp the fundamentals before moving onto code alongs to see the application of these fundamentals. Once you have done so, you are ready to proceed with this code along!
Learning objectives
In this code along, we will prepare to implement cellular automata, which are represented as two-dimensional arrays. To do so, we will learn how to extend Python’s list syntax to implement two- and multi-dimensional arrays. After learning some multi-dimensional list basics, we will then write a function to print out a Game of Life board.
Code along summary
Setup
Create a folder called two_d_arrays
in your python/src/
directory. Create a text file called main.py
in the python/src/two_d_arrays
folder. We will edit main.py
in this code along, which should have the following starter code.
def main(): print("Two-dimensional arrays.") if __name__ == "__main__": main()
Working with two-dimensional tuples
Python contains support for multi-dimensional arrays via immutable tuples and mutable lists; we will typically work with the latter, but will give an example of the former. In the next chapter’s homework assignment, we will work with the mysterious 3 x 3 array shown in the table below, which will help us understand how a zebra’s stripes can appear from simple patterns.
0.05 | 0.2 | 0.05 |
0.2 | 0 | 0.2 |
0 | 0.2 | 0.05 |
Let’s declare this array as a tuple called kernel
in Python, updating main()
as follows. Note that we declare the middle element as 0.0 so that Python reads it as a float
instead of as an int
.
def main(): print("Two-dimensional arrays.") kernel = ( (0.05, 0.20, 0.05), (0.20, 0.00, 0.20), (0.05, 0.20, 0.05), ) print(kernel) if __name__ == "__main__": main()
STOP: In a new terminal window, navigate into our directory usingcd python/src/two_d_arrays
. Then run your code by executingpython3 main.py
(macOS/Linux) orpython main.py
(Python). You should see((0.05, 0.2, 0.05), (0.2, 0.0, 0.2), (0.05, 0.2, 0.05))
printed to the console. We will discuss a prettier way of printing two-dimensional arrays soon.
Accessing individual elements and rows
Both tuples and lists are 0-indexed, and we can access the element of a
in row r
and column c
using a[r][c]
. Let’s print the values of our previous 3 x 3 array that are highlighted in the table below, which correspond to kernel[0][2]
in the top right, kernel[1][1]
in the middle, and kernel[2][1]
in the bottom middle.
0.05 | 0.2 | 0.05 |
0.2 | 0 | 0.2 |
0.05 | 0.2 | 0.05 |
def main(): print("Two-dimensional arrays.") kernel = ( (0.05, 0.20, 0.05), (0.20, 0.00, 0.20), (0.05, 0.20, 0.05), ) print(kernel) print(kernel[0][2], kernel[1][1], kernel[2][1]) # prints 0.05 0.0 0.2 if __name__ == "__main__": main()
Although we can access individual elements of a tuple, we cannot change them because of the immutability of tuples; for example, the line of code kernel[1][0] = 0.7
will produce an error.
We can isolate the row of a list or tuple a
at index r
using the notation a[r]
; let’s use this to print the second row of our tuple, kernel[1]
, as follows.
def main(): print("Two-dimensional arrays.") kernel = ( (0.05, 0.20, 0.05), (0.20, 0.00, 0.20), (0.05, 0.20, 0.05), ) print(kernel) print(kernel[0][2], kernel[1][1], kernel[2][1]) # prints 0.05 0.0 0.2 print(kernel[1]) # prints (0.2, 0.0, 0.2) if __name__ == "__main__": main()
The wrong way to declare two-dimensional lists
Consider the array in the figure below, reproduced from the core text, which has 7 rows and 4 columns.

Remember that we can declare a one-dimensional list a
with 7 zeroes using the notation a = [0] * 7
.
Let’s therefore try the following to declare a 7 x 4 list of zeroes.
a = [[0] * 4] * 7
Next, since we know that lists are mutable, let’s change a[1][3]
to be equal to 5, and then print a
.
def main(): # code omitted for clarity a = [[0] * 4] * 7 a[1][3] = 5 print(a)
STOP: What do you think will be printed?
When we print a
, we see the following output.
[[0, 0, 0, 5], [0, 0, 0, 5], [0, 0, 0, 5], [0, 0, 0, 5], [0, 0, 0, 5], [0, 0, 0, 5], [0, 0, 0, 5]]
What in the world is going on? When we execute the line a = [[0] * 4] * 7
, you might imagine that Python creates seven one-dimensional lists, each of which has four zeroes. However, what it actually does is creates one one-dimensional list with four zeroes, and then it creates seven references to it. In other words, all these references refer to the same one-dimensional list. As a result, modifying one row at index 3, changes all of the rows of a
at index 3, since all the rows point to the same underlying object.
If this seems confusing, it is because that it is. In fact, it is an utterly horrible and reasonably criticized feature of Python. We show it to you not to criticize Python but rather so that you appreciate that this property of the language exists, so that you can avoid it, and so that we can show you the right way to create a two-dimensional list.
Working with two-dimensional lists
Python provides a number of ways to define a two-dimensional list without the above unexpected behavior, and we will show one. After making a blank list a
, we will use a for loop to range over the number of rows that we want to create, and in each one, we create the row, and then append it to a
.
def main(): # code omitted for clarity a = [] for row in range(7): new_row = [0] * 4 a.append(new_row)
Since lists are mutable, we can now set a few values of a
, as shown below.
def main(): # code omitted for clarity a = [] for row in range(7): new_row = [0] * 4 a.append(new_row) a[1][2] = 19 a[0][0] = 42 a[6][3] = 100
After these updates, the list has the values as illustrated in the table below.
42 | 0 | 0 | 0 |
0 | 0 | 19 | 0 |
0 | 0 | 0 | 0 |
0 | 0 | 0 | 0 |
0 | 0 | 0 | 0 |
0 | 0 | 0 | 0 |
0 | 0 | 0 | 100 |
a
after updating the values a[0][0]
, a[1][2]
, and a[6][3]
.We can print the array and see these values by using a print statement.
def main(): # code omitted for clarity a = [] for row in range(7): new_row = [0] * 4 a.append(new_row) a[1][2] = 19 a[0][0] = 42 a[6][3] = 100 print(a)
When we run our code, we see the following printed.
[[42, 0, 0, 0], [0, 0, 19, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 100]]
Array rows and columns
When a
is a one-dimensional array, len(a)
gives the number of elements in a
. When a
is a two-dimensional array, len(a)
gives the number of rows in a
. Because a
is rectangular, we can access the number of its columns by accessing the length of any row.
def main(): # code omitted for clarity print("Number of rows:", len(a)) # prints 7 print("Number of columns:", len(a[0])) # prints 4
Declaring non-rectangular lists
One thing that is nice about lists is that we are not required to have every row contain the same number of elements. The following code will set the number of elements in each row to be equal to the row index, each with the value False
.
def main(): # ... num_rows = 4 board = [] # make all the rows of board for row in range(num_rows): board.append([False] * row) print(board)
Now when we run our code, we see that [[], [False], [False, False], [False, False, False]]
is printed. We have created a “non-rectangular” array (in fact, a triangular array) that has zero, one, two, and three elements in its four rows, as illustrated below.
(empty) | ||
False | ||
False | False | |
False | False | False |
Appending to a two-dimensional list
Because the first row of board
is empty, let’s add a False
element to the first row. To maintain our triangular shape, let’s also add a False
element to each of the other rows.
We are appending a False
value to each row of board (a one-dimensional boolean array). We can therefore range over each row board[row]
and append False
to it.
def main(): # ... num_rows = 4 board = [] # make all the rows of board for row in range(num_rows): board.append([False] * row) # let's append a false value to each row for row in range(len(board)): board[row].append(False) print(board)
When we run our code, we see that board
has the values as shown in the figure below.
False | |||
False | False | ||
False | False | False | |
False | False | False | False |
Let’s now add an entire row of length 5 to board
, preserving the triangular shape. Because board
can be thought of as a one-dimensional array of one-dimensional arrays of boolean values, we just need to append one more array of length 5 to board
.
def main(): # ... num_rows = 4 board = [] # make all the rows of board for row in range(num_rows): board.append([False] * row) # let's append a false value to each row for row in range(len(board)): board[row].append(False) # let's append an entire row to the list new_row = [] for _ in range(5): new_row.append(False) board.append(new_row) print(board)
When we run our code once more, we see that we have added the array of five False
values as shown below.
False | ||||
False | False | |||
False | False | False | ||
False | False | False | False | |
False | False | False | False | False |
Lists as function inputs
Consider the following function that takes as input a two-dimensional boolean list a
and sets its top left element to True
, without returning the list. Note that the type hint for a
is a: list[list[bool]]
.
def set_first_element_to_true(a: list[list[bool]]) -> None: """ Set the top-left element of a 2D boolean list to True. Args: a (list[list[bool]]): A 2D list of boolean values. Raises: ValueError: If the board is empty or the first row has no elements. Returns: None: The function modifies the input list in place. """ if len(a) == 0 or len(a[0]) == 0: raise ValueError("Board is empty or malformed") a[0][0] = True
In def main()
, let’s pass in our triangular list board
as input to this function and then print the result.
def main(): # ... set_first_element_to_true(board) print(board)
STOP: What do you think will be printed?
You may recall from our introduction to lists that lists are “pass-by-reference”. When we pass board
into set_first_element_to_true()
, any changes that we make to board
inside of the function will be visible outside of the function as well. As a result, when we print board
after passing it into this function, we will see that its top left element has changed to True
, as shown below.
True | ||||
False | False | |||
False | False | False | ||
False | False | False | False | |
False | False | False | False | False |
Printing a Game of Life board using functions
We now know everything that we need to know about two-dimensional lists in order to implement cellular automata in the upcoming code alongs. Before we continue, however, let’s practice what we have learned to print a two-dimensional list in a nicer form than what we have seen previously.
In the core text, we saw that one way of printing a two-dimensional list would use a nested for loop. At the level of pseudocode, this function, which we call PrintBoard()
, looked like the following.
PrintBoard(board) for every integer r from 0 to CountRows(board) - 1 for every integer c from 0 to CountCols(board) - 1 Print(board[r][c]) Print(new line)
In Python, when we range over a two-dimensional list board
, we range over its rows. For a given row board[r]
, we can then range over this row to obtain all the values in this row. This idea can be implemented in the following function.
In implementing this function, we need to print the values of individual cells of the board without printing a new line. To do so, we need to introduce a second argument to the built-in print()
function. Specifically, print(pattern, end = separator)
will print the string pattern
, followed by the value of the string separator
, without printing a new line. In this case, when we print a cell’s value, we only want to print a space after its value, so our separator
is a single space, " "
. We therefore will print a cell by calling print(board[r][c], end=" ")
.
def print_board(board: list[list[bool]]) -> None: """ Print a 2D boolean game board to the console, with each row starting a new line. Each element in the board represents a cell: Args: board (list[list[bool]]): A 2D list representing the board. Returns: None: This function only prints the board to the console. """ for r in range(len(board)): for c in range(len(board[r])): print(board[r][c], end=" ") print() # new line after finishing a row
Note: When we callprint()
normally withoutend
specified, there is an invisibleend = "\n"
included, where \n represents the “new line character”.
We also saw in the core text that this approach is not the most intuitive, since we need to make sure to remember to print a new line, and since nested for loops can be tricky to parse.
Instead, we planned our code in a modular fashion, as shown below. After ranging over the rows, we pass the work of printing the current row to a subroutine print_row
()
. In this function, we range over the elements of the row, printing each one with another subroutine print_cell
()
, and then printing a new line. As for print_cell
()
, it just checks whether the current cell is True
or False
and prints a differently-colored square accordingly.
PrintBoard(board) for every integer r from 0 to CountRows(board) - 1 PrintRow(board[r]) PrintRow(row) for every integer c from 0 to len(row) - 1 PrintCell(row[c]) Print(new line) PrintCell(value) if value = true Print("⬛") else Print("⬜")
We will now implement these functions in Python below, starting with print_board()
and print_row()
.
def print_board(board: list[list[bool]]) -> None: """ Print a 2D boolean board to the console. Args: board (list[list[bool]]): A 2D list of boolean values representing the board. Returns: None: This function prints the board row by row. """ for row in board: print_row(row) def print_row(row: list[bool]) -> None: """ Print a single row of the board. Args: row (list[bool]): A list of boolean values representing one row of the board. Returns: None: This function prints the row and then moves to a new line. """ for val in row: print_cell(val) print() # ensures printing a new line at end of each row
We could print white and black squares (⬜ ⬛) to represent alive and dead cells, but let’s instead print two emojis. We will use 😍 to represent an alive cell and 💀 to represent a dead cell. Note that in this case, we don’t need to print a space between the emojis, and so we use end=""
.
def print_cell(value: bool) -> None: """ Print a single cell of the board. Args: value (bool): A boolean representing the state of the cell. True (alive) prints 😍, False (dead) prints 💀. Returns: None: This function prints the cell symbol without a newline. """ if value: print("😍", end="") # to prevent printing a new line end="" else: print("💀", end="")
Printing the R pentomino
The R pentomino is shown below on the below, along with the table of boolean values corresponding to this pattern shown beneath it.

False | False | False | False | False |
False | False | True | True | False |
False | True | True | False | False |
False | False | True | False | False |
False | False | False | False | False |
Let’s practice our work with two-dimensional arrays by declaring the R pentomino, setting its value, and then calling print_board()
on it.
def main(): # ... num_rows = 5 num_cols = 5 r_pentomino = [] # make all the rows of board for row in range(num_rows): r_pentomino.append([False] * num_cols) r_pentomino[1][2] = True r_pentomino[1][3] = True r_pentomino[2][1] = True r_pentomino[2][2] = True r_pentomino[3][2] = True print_board(r_pentomino)
When we run our code, we see the following printed to the console.

Looking ahead
We now have a nicer picture of the R pentomino, but we would like to make a drawing that allows us to animate the Game of Life over multiple generations. This will be our focus in the next code along, where we talk about built-in packages for drawing and prepare ourselves to implement the Game of Life.