Introduction to Two-Dimensional Arrays in Python

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.050.20.05
0.200.2
00.20.05
A 3 x 3 array that we will find useful in the next chapter.

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 using cd python/src/two_d_arrays. Then run your code by executing python3 main.py (macOS/Linux) or python 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.050.20.05
0.200.2
0.050.20.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.

A 7 x 4 array (denoted a) with seven rows and four columns. The rows are indexed 0 to 6, and the columns are indexed 0 to 3; as a result, the highlighted element is referred to as a[1][2]. This array can be thought of as a one-dimensional array of length 7, where every element is a row represented as a one-dimensional array of length 4.

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.

42000
00190
0000
0000
0000
0000
000100
The array 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
FalseFalse
FalseFalseFalse

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
FalseFalse
FalseFalseFalse
FalseFalseFalseFalse

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
FalseFalse
FalseFalseFalse
FalseFalseFalseFalse
FalseFalseFalseFalseFalse

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
FalseFalse
FalseFalseFalse
FalseFalseFalseFalse
FalseFalseFalseFalseFalse

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 call print() normally without end specified, there is an invisible end = "\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.

This image has an empty alt attribute; its file name is R_pentomino.jpg
The R pentomino.
FalseFalseFalseFalseFalse
FalseFalseTrueTrueFalse
FalseTrueTrueFalseFalse
FalseFalseTrueFalseFalse
FalseFalseFalseFalseFalse
The implementation of the R pentomino as a 5 x 5 array of Boolean values.

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.

Page Contents
Scroll to Top