Fundamentals
Matrices in Python with Lists of Lists: Rows and Columns
Learn to create, access and go through matrices in Python with lists of lists, avoid the asterisk trap and see real uses on the ESP32.
By Prof. Rodrigo Faustino · Updated on · 19 min read
Metrics for this article
- Words
- 3,784
- Characters
- 19,909
- Reading time
- ~19 min
A matrix is a table of values organized in rows and columns. Python has no type of its own for it: you use a list of lists, in which each inner list is a row. To reach a value, you write two indexes, m[i][j], the first for the row and the second for the column. Almost everything you already know about lists still holds, and what is new comes down to two things: going through the table with one loop inside another and being careful with a classic error when creating the matrix.
Matrices show up whenever the data has two dimensions: the key map of a numeric keypad, the pixels of a drawing on a screen, the board of a game, the grades of several students in several tests, a grid of LEDs. This article shows how to create, read, change and go through a matrix, how to print it in a readable way, how to add up rows and columns, where the asterisk trap is, and three uses that fit on the ESP32. All the examples were run in the simulator, and the results quoted are the ones it produces.
A list of lists
Each row of the matrix is an ordinary list, and the matrix is a list that holds these rows:
m = [
[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12]
]
print(m)
print(len(m), len(m[0]))
print(m[0], m[1][2], m[-1][-1], m[2][0])
print(m[1])
coluna = [linha[1] for linha in m]
print(coluna)
print(type(m), type(m[0]))
print("end")The output is [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]], then 3 4, then [1, 2, 3, 4] 7 12 9, then [5, 6, 7, 8], then [2, 6, 10], then <class 'list'> <class 'list'> and finally end.
It is worth reading each line slowly. The len(m) counts how many rows there are, because m is the outer list. The len(m[0]) counts how many columns there are, because m[0] is the first row and its length is the number of elements in it. The two numbers together, 3 4, describe a matrix of 3 rows by 4 columns.
Access works in two steps. The m[1] returns the row at index 1, which is the list [5, 6, 7, 8]. On that list, the [2] picks the element at index 2, the 7. That is why m[1][2] is just the compact way of writing (m[1])[2]. Negative indexes follow the usual rule: m[-1] is the last row and m[-1][-1] is the last element of it, the 12.
There is an asymmetry that tends to surprise people. Getting a row is easy, it is just m[1]. Getting a column has no shortcut, because columns do not exist as objects: they are scattered, one element in each list. To gather them, you go through the rows and take the element at the wanted position, as in the comprehension [linha[1] for linha in m], which returns [2, 6, 10]. This feature was seen in the article on list operations, and here it gets a good reason to exist.
Notice also that the type of both answers is list. There is no "matrix" object behind it: it is a list whose elements happen to be lists. This explains everything that follows, including the errors.
From the syllabus to a planned semester
- Teaching plan and calendar built automatically
- Class diary ready to paste into SIGA
- Slides and exercise lists with answer keys
Free Community edition · Windows
Changing an element
Since the list is mutable, the matrix is too, and assignment uses the same two indexes:
m = [[1, 2, 3], [4, 5, 6]]
m[0][1] = 99
m[1][2] += 10
print(m)
m[1] = [0, 0, 0]
print(m)
m[0].append(7)
print(m, len(m[0]), len(m[1]))
print("end")The output is [[1, 99, 3], [4, 5, 16]], then [[1, 99, 3], [0, 0, 0]], then [[1, 99, 3, 7], [0, 0, 0]] 4 3 and finally end.
The first line swaps an element, the second adds 10 to another. In the third, m[1] = [0, 0, 0] replaces a whole row with another list. In the fourth, the append acts on m[0], which is a list like any other, and adds an element to that row only. The result is a table in which the rows have different sizes, which Python accepts without complaint. We will come back to this further on, because a "true" matrix has all rows of the same size, and it is the programmer, not the language, who guarantees that.
Going through it with nested loops
To visit every element, you use one loop inside the other. The outer one walks through the rows and the inner one walks through the columns of the current row:
m = [[1, 2, 3], [4, 5, 6]]
for linha in m:
print(linha)
for linha in m:
for valor in linha:
print(valor, end=" ")
print()
for i in range(len(m)):
for j in range(len(m[i])):
print(f"m[{i}][{j}] = {m[i][j]}")
for i, linha in enumerate(m):
for j, valor in enumerate(linha):
if valor % 2 == 0:
print("even at", i, j)
print("end")The output brings [1, 2, 3] and [4, 5, 6] from the first loop, then the lines 1 2 3 and 4 5 6 from the second, then six lines from m[0][0] = 1 to m[1][2] = 6, then even at 0 1, even at 1 0 and even at 1 2, and finally end.
The four loops make the same walk in different ways, and each one serves a purpose:
for linha in mhands over each whole row, as a list. It serves when the work is by row, such as adding up or printing.for linha in mwithfor valor in linhahands over each value, without knowing the position. It is the cleanest way when only the value matters.for i in range(len(m))withfor j in range(len(m[i]))gives the indexes. It is necessary when the position enters the calculation, or when the program is going to change the matrix, since the loop'svaloris only a copy of the number and changing it does not change the matrix.enumerategives the index and the value at the same time, without having to writem[i][j]every time.
The print(valor, end=" ") of the second loop writes the values on the same line, separated by a space. The empty print(), at the end of each turn of the outer loop, moves to a new line. The position of this print() is the detail that causes the most confusion: it needs to be aligned with the for valor, and not inside it. Inside it, each number would come on its own line, and the result would stop looking like a table. The article on the for loop with range and the explanation of why repeat with loops help review the mechanics of the simple loop before stacking two of them.
The cost is easy to work out: with 2 rows and 3 columns, the inner loop runs 3 times for each of the 2 turns of the outer one, for a total of 6 steps. In a 100 by 100 matrix, that is 10 thousand. For small tables this weighs nothing, but it is good to have a sense that the work grows with the product of the two dimensions.
Showing the matrix as a table
Printing the matrix with print(m) shows the brackets and commas, which helps with debugging, but does not look like a table. To see aligned rows and columns, you print row by row:
m = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
for linha in m:
print(*linha)
for linha in m:
print(" ".join(str(v) for v in linha))
for linha in m:
print("".join(f"{v:4d}" for v in linha))
print("end")The output has three blocks of three lines. The first and the second are equal, 1 2 3, 4 5 6 and 7 8 9. The third reserves 4 positions for each number and comes out as 1 2 3, 4 5 6 and 7 8 9.
The print(*linha) is the shortest: the asterisk unpacks the list into separate arguments, and print writes them with a space between them. The join does the same, but requires each item to be text, hence the str(v). The f-string with {v:4d} gives each number a fixed width, which aligns the columns even when the values have different numbers of digits. For tables mixing numbers of 1 and 2 digits, it is the one that leaves the result nicest. The article on strings deals with formatting in more detail and shows what MicroPython lacks for aligning text.
The AI that remembers your life and lives on your computer
- Long-term memory with the source of every answer
- Works offline: your data stays on your PC
- Reads PDFs, slides, photos and meeting recordings
Essential plan free forever
Creating a matrix without having the values ready
Sometimes the matrix is not written by hand. The program needs to create a 5 by 5 one full of zeros, or a multiplication table, and fill it in afterwards. Here lives the best-known trap of the subject.
errada = [[0] * 3] * 2
errada[0][0] = 1
print(errada, errada[0] is errada[1])
certa = [[0] * 3 for _ in range(2)]
certa[0][0] = 1
print(certa, certa[0] is certa[1])
tabuada = [[i * j for j in range(1, 4)] for i in range(1, 4)]
print(tabuada)
vazia = []
for i in range(2):
linha = []
for j in range(3):
linha.append(0)
vazia.append(linha)
print(vazia)
print("end")(The names mean errada = wrong, certa = right, tabuada = multiplication table and vazia = empty.) The output is [[1, 0, 0], [1, 0, 0]] True, then [[1, 0, 0], [0, 0, 0]] False, then [[1, 2, 3], [2, 4, 6], [3, 6, 9]], then [[0, 0, 0], [0, 0, 0]] and finally end.
In the first matrix, only one position was changed, but two changed. The reason is in what the * does. In [0] * 3, the list of three zeros is created once. In [[0] * 3] * 2, the * 2 does not create two lists: it puts the same list in two places. errada[0] and errada[1] are two names for a single object, and the is operator confirms it with True. Changing one row changes "both", because there never were two.
With numbers, the * works with no problem, and that is why the error is deceptive: [0] * 3 is safe, since the 0 is immutable, and nobody suspects that [[0] * 3] * 2 is different. The difference only shows up when the repeated element is mutable, and a list is. The article on lists showed the same idea with b = a: two names, one object.
The second form, [[0] * 3 for _ in range(2)], solves it because the expression [0] * 3 is evaluated again on each turn of the loop, and each turn creates a new list. The False of the is shows that the two rows are distinct objects. The _ is just a name, by convention, for the loop variable that will not be used. The result is a matrix in which changing one row leaves the others in peace.
The third creation, the tabuada, shows the comprehension with two levels. The outer loop, for i in range(1, 4), creates each row, and the inner one, for j in range(1, 4), creates its elements. The reading is: "for each i, a row with i * j for each j". The fourth, with append, does the same in an explicit way, and is the one that tends to be easier to debug when the logic of the row is long. Both give equivalent results, and either of them is a correct way to build the matrix.
Copying a matrix
The same idea explains the behavior of copying. The copy() method of a list of lists is shallow: it copies the outer list, but the rows are still the same ones:
a = [[1, 2], [3, 4]]
b = a.copy()
b[0][0] = 99
print(a, b)
print(a is b, a[0] is b[0])
c = [linha[:] for linha in a]
c[1][1] = 77
print(a)
print(c)
irregular = [[1], [2, 3], [4, 5, 6]]
print([len(linha) for linha in irregular])
print("end")The output is [[99, 2], [3, 4]] [[99, 2], [3, 4]], then False True, then [[99, 2], [3, 4]], then [[99, 2], [3, 77]], then [1, 2, 3] and finally end.
The b = a.copy() created a new list, and that is why a is b gives False. But the first row of a and the first row of b are the same object, which a[0] is b[0] confirms with True. Changing b[0][0] therefore touched both matrices. The deep copy of a matrix is made by copying each row too: [linha[:] for linha in a], where linha[:] is the slice that creates a copy of each row. After that, changing c[1][1] leaves a as it was.
The last part of the example shows an irregular list of lists, with rows of 1, 2 and 3 elements. Python accepts it with no problem, and it is useful in some cases, such as the list of grades of each student when each one took a different number of tests. But almost all matrix code assumes that len(m[0]) holds for every row. If your program assumes this, it is worth guaranteeing it.
Adding up rows, columns and the total
With the matrix gone through, the calculations by row and by column come straight from the list:
m = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
print(sum(m[0]), sum(m[1]))
print([sum(linha) for linha in m])
print([sum(linha[j] for linha in m) for j in range(len(m[0]))])
total = 0
for linha in m:
total += sum(linha)
print(total)
print(max(max(linha) for linha in m), min(min(linha) for linha in m))
print("end")The output is 6 15, then [6, 15, 24], then [12, 15, 18], then 45, then 9 1 and finally end.
The sum of one row is simple, because it is already a list: sum(m[0]). That of all the rows is a comprehension that applies sum to each one. That of the columns needs more care, because column j has to be assembled: for each j, you add up the element at position j of all the rows, and the range(len(m[0])) gives the values of j. The total can be calculated by adding up the sums of the rows, and the largest and smallest value of the matrix come from a max over the maximums of the rows, and a min over the minimums.
This kind of calculation is the basis of the challenges Matrix Sum and Zero Counter, which ask for a nested loop with an accumulator and a counter. Matrix Diagonal asks for the elements in which the row index equals the column index, and adding matrices and swapping rows for columns are left for the next article.
On the ESP32: keypad, drawing and LED grid
Three uses of a matrix that fit well on a board with few pins:
from machine import Pin
TECLAS = [["1", "2", "3"], ["4", "5", "6"], ["7", "8", "9"], ["*", "0", "#"]]
def tecla(linha, coluna):
return TECLAS[linha][coluna]
print(tecla(0, 0), tecla(2, 1), tecla(3, 2))
SETA = [
[0, 0, 1, 0, 0],
[0, 1, 1, 1, 0],
[1, 0, 1, 0, 1],
[0, 0, 1, 0, 0],
[0, 0, 1, 0, 0]
]
for linha in SETA:
print("".join("#" if v else "." for v in linha))
LEDS = [[Pin(18, Pin.OUT), Pin(19, Pin.OUT)], [Pin(21, Pin.OUT), Pin(22, Pin.OUT)]]
for linha in LEDS:
for led in linha:
led.value(0)
print(len(LEDS), len(LEDS[0]))
print("end")(The names TECLAS = keys and SETA = arrow.) The output is 1 8 #, then five lines, ..#.., .###., #.#.#, ..#.. and ..#.., then 2 2 and finally end.
The numeric keypad of 4 rows and 3 columns is read by scanning: the program turns on one row at a time, sees which column responds and finds the (row, column) position of the pressed key. The TECLAS matrix translates that position into the character, and the tecla function is just the access TECLAS[linha][coluna]. Without the matrix, you would need an if for each of the 12 keys.
The 5 by 5 drawing, the SETA, is a pixel map: 1 lights up, 0 turns off. The program goes through it and swaps each value for # or ., which shows the image on the console. On a real display, the same loop would light up dots instead of writing characters, and the drawing grows in size by changing only the numbers, without touching the logic. The article on the OLED and LCD display shows how to take a drawing like this to a screen.
The LED grid holds the Pin objects already organized in rows and columns. This way, lighting the LED at row 1 and column 0 is LEDS[1][0].value(1), and lighting all those of a row is a simple loop. The example only prepares the pins and turns them all off, to validate the structure. In a real circuit, the pins depend on the wiring and on the number of LEDs, and each output needs its resistor, as the article on PWM, LED and buzzer shows.
Common errors
The first error is asking for a position that does not exist. In a 2 by 2 matrix, only the indexes 0 and 1 are valid, at either of the two levels:
m = [[1, 2], [3, 4]]
print(m[2][0])IndexError: list index out of rangeThe same goes for m[0][5]: the row exists, but the column does not. The message is the same in both cases, and does not say which of the two indexes failed. Whoever reads the error needs to look at the line and check the sizes with len(m) and len(m[0]).
The second error comes from those who use the writing of other languages, with both indexes inside the same bracket:
m = [[1, 2], [3, 4]]
print(m[0, 1])TypeError: list indices must be integers, not tupleThe m[0, 1] is a single index, and that index is the tuple (0, 1), which the list does not accept. In the computer's Python, the text of the message is a little different, list indices must be integers or slices, not tuple, but the error and the fix are the same: write m[0][1].
The third is indexing a number as if it were a list:
m = [[1, 2], [3, 4]]
print(m[0][0][0])TypeError: 'int' object isn't subscriptableAfter m[0][0] you have already reached the 1, which is an integer, and the third [0] has nowhere to go. The computer's Python writes 'int' object is not subscriptable. This message usually indicates that the code was written for a matrix with more levels than the one that exists.
The fourth shows up when the list has an element that is not a list. If the program does m.append(5) and then goes through it with the nested loop, it prints 1 2 3 4 and stops on reaching the 5:
TypeError: 'int' object isn't iterableThe inner loop tries to go through the 5 as if it were a row, and a number cannot be gone through. The computer's Python message says 'int' object is not iterable. It is the sign that a row was replaced by a loose value.
Two behaviors that are not errors, but cause confusion: m[0] + m[1] joins the two rows into a single list and gives [1, 2, 3, 4], and m[0] * 2 repeats the row and gives [1, 2, 1, 2]. The + and the * of lists do not do matrix arithmetic element by element. As for m + 1, it fails, with unsupported types for __add__: 'list', 'int' in the simulator and can only concatenate list (not "int") to list in the computer's Python. Adding two real matrices requires a loop, and that is the subject of the next article.
What tends to show up in class
These are the patterns that repeat most with people learning matrices. They are not the account of a specific class, but the difficulties that always appear.
The [[0] * 3] * 2. It is the champion. The person creates the matrix of zeros, changes an element, prints and sees the value repeated in another row. The typical reaction is to think that the print is wrong or that the loop ran twice. The way back is the test m[0] is m[1], which shows at once that the rows are the same one, followed by swapping to the comprehension with a loop.
Swapping i and j. Writing m[j][i] where it should be m[i][j]. In a square matrix, the program runs and delivers wrong numbers, which is worse than stopping with an error. In a non-square one, the IndexError appears, and it is what exposes the swap. A habit that helps is giving names that recall the order, such as lin and col, and always writing the row first.
Confusing len(m) with len(m[0]). The first is the number of rows and the second, of columns. Whoever uses range(len(m)) in both loops works on square matrices and breaks on the others. The rule is: the outer loop uses len(m) and the inner one uses len(m[i]).
Testing only with a 2 by 2 matrix. A small, square matrix hides almost all the errors above, because i and j have the same range and swapping them breaks nothing. Testing with one of 2 rows by 3 columns, or 3 by 2, uncovers many more problems for the same effort.
The m[0, 1]. It comes from those who have already programmed in another language or used spreadsheets. The type error with the tuple is solved quickly when you see that these are two brackets, one for each list.
Printing the raw list of lists. The result of print(m) is correct, but nobody can check it by eye in a 5 by 5 matrix. Printing row by row, with print(*linha), turns the checking into looking at a table.
Forgetting to create the row. In m[i][j] = valor on an empty matrix, the error is one of index, because the rows do not exist yet. Either you create the matrix with the right size from the start, or you add each row with append before filling it in.
Changing the shallow copy. Whoever does b = a.copy() and changes b finds that a changed too. The habit to build is to ask, before copying, whether the elements are lists.
Exercise: reading, adding up and checking a matrix
Write a program that reads the number of rows, the number of columns and then the values, one at a time, builds the matrix, prints it as a table and shows the sum of each row, the sum of each column and the total. The solution below shows one path, and the entradas= of the block provides the values: 2 rows, 3 columns and the numbers from 1 to 6.
linhas = int(input())
colunas = int(input())
m = []
for i in range(linhas):
linha = []
for j in range(colunas):
linha.append(int(input()))
m.append(linha)
print("matrix", m)
for linha in m:
print(*linha)
print("sum of rows", [sum(linha) for linha in m])
print("sum of columns", [sum(m[i][j] for i in range(linhas)) for j in range(colunas)])
total = 0
for linha in m:
total += sum(linha)
print("total", total)
print("end")The simulator repeats the typed values in the output, one per line, and then comes matrix [[1, 2, 3], [4, 5, 6]], then 1 2 3, then 4 5 6, then sum of rows [6, 15], then sum of columns [5, 7, 9], then total 21 and finally end.
Notice four decisions. The row is created inside the outer loop, and so each turn has its own list, which avoids the asterisk trap. Each value goes through int(input()), since input returns text. The numbers of rows and columns are stored in variables, and it is colunas that controls the inner loop, and not a fixed value. And the columns are added up with m[i][j] sweeping i, which works because the program knows how many rows there are.
To practice beyond that, try: changing the inputs to 3 rows and 2 columns and seeing whether everything is still right; adding a line with the largest value of each row; counting how many elements are even; and printing the matrix with a width of 4 positions per number.
Frequently asked questions
Does Python have a matrix type? Not in the core of the language. The list of lists is the standard way. On computers, libraries such as NumPy offer complete numeric matrices, but they do not exist in the simulator, and for small tables the list of lists does the job.
How do I know how many rows and columns the matrix has? The number of rows is len(m), and the number of columns is len(m[0]), as long as the matrix is not empty and all rows have the same size. With m = [], the len(m[0]) gives IndexError, because there is no first row.
Why does a change in one row show up in another? Because the two rows are the same object, almost always because of [[0] * 3] * 2 or a shallow copy. The test m[0] is m[1] confirms it, and creating with a comprehension or append in a loop fixes it.
Can I have a matrix of texts or of mixed values? You can. The list accepts anything in each position, and the keypad example uses texts. Mixing types in the same matrix, however, complicates the calculations, and is only worth it if the program handles each case.
Do I really need range(len(m)), or can I use for linha in m? If only the value matters, the direct for is cleaner. If the position matters, or if the matrix will be changed during the walk, use the indexes or enumerate.
Is there a matrix with more than two dimensions? There is: a list of lists of lists, accessed with m[i][j][k]. It works the same way, just with one more level. For most board programs, two dimensions are enough.
Where to go next
The challenges Matrix Diagonal, Matrix Sum and Zero Counter practice access by two indexes and the walk with nested loops. To review what supports this article, it is worth going through the texts on lists, list operations and tuples, and through the Logic with Python track.
The next article continues with matrices: adding two matrices, transposition, which swaps rows for columns, and how to take the main and secondary diagonals.