List of Lists in Python (Create, Append, Index, Loop)

A list of lists in Python is just a list whose items are other lists, which gives you rows and columns like a small spreadsheet. You create one with nested square brackets and reach a value with two indexes, row first:

sales = [["Seattle", 1250.50], ["Chicago", 2100.00]]
sales[1][0]     # 'Chicago'

I ran the code below on Python 3.12.5 in a Windows Command Prompt, using made-up quarterly sales for a few US cities in USD.

You’ll see how to build, index, append to, loop over, copy, sort and flatten a nested list, plus the one bug that catches almost everyone.

How to create a list of lists in Python

Put each row in its own brackets and separate the rows with commas. The first index chooses the row and the second chooses the column inside it:

sales = [
    ["Seattle", 1250.50, 980.25],
    ["Chicago", 2100.00, 1875.40],
    ["Miami", 875.75, 1320.10],
]

print(sales[0])          # the first row
print(sales[1][0])       # first item of the second row
print(sales[-1][-1])     # last value of the last row
print(len(sales), "rows,", len(sales[0]), "columns")

empty = [[] for _ in range(3)]
print(empty)

Output:

['Seattle', 1250.5, 980.25]
Chicago
1320.1
3 rows, 3 columns
[[], [], []]
Command Prompt output of a Python list of lists holding US city sales, indexed by row and column
sales[1][0] is Chicago, and negative indexes count from the end of each list.

For an empty grid you’ll fill later, a comprehension like [[] for _ in range(3)] gives you three separate empty rows. Why that matters comes up in the multiply section below.

How do you append to a list of lists?

It depends on what you’re adding. Call append() on the outer list to add a row, or on one inner list to add a value to that row:

sales = [["Seattle", 1250.50], ["Chicago", 2100.00]]

sales.append(["Miami", 875.75])        # add a whole new row
sales[0].append(980.25)                # add a value to one row
sales.insert(0, ["Denver", 640.00])    # new first row
sales.extend([["Austin", 1500.00], ["Boston", 990.60]])

for row in sales:
    print(row)

Output:

['Denver', 640.0]
['Seattle', 1250.5, 980.25]
['Chicago', 2100.0]
['Miami', 875.75]
['Austin', 1500.0]
['Boston', 990.6]
Command Prompt output after appending rows and values to a Python list of lists with append, insert and extend
append() adds one row, extend() adds several, and sales[0].append() grows just the first row.

Rows don’t have to be the same length, which is handy but easy to forget when you loop. Appending multiple items to a list has more on extend().

Looping through a list of lists

When every row has the same shape, unpack it right in the for line. If you need positions, nest two loops with enumerate():

sales = [
    ["Seattle", 1250.50, 980.25],
    ["Chicago", 2100.00, 1875.40],
    ["Miami", 875.75, 1320.10],
]

for city, q1, q2 in sales:
    print(f"{city:<8} Q1 ${q1:>9,.2f}  Q2 ${q2:>9,.2f}  total ${q1 + q2:>9,.2f}")

for r, row in enumerate(sales):
    for c, value in enumerate(row):
        if value == 2100.00:
            print("found 2100.00 at row", r, "column", c)

Output:

Seattle  Q1 $ 1,250.50  Q2 $   980.25  total $ 2,230.75
Chicago  Q1 $ 2,100.00  Q2 $ 1,875.40  total $ 3,975.40
Miami    Q1 $   875.75  Q2 $ 1,320.10  total $ 2,195.85
found 2100.00 at row 1 column 1

I like the unpacked version for reports because the names say what each column is. The Python for loop guide covers enumerate() in more detail.

Why [[0] * 3] * 3 breaks a list of lists

Multiplying a list repeats references to the same object. So [[0] * 3] * 3 gives you three rows that are really one list, and changing one changes them all:

grid = [[0] * 3] * 3       # looks fine...
grid[0][0] = 9
print(grid)                # ...but all three rows changed

print(grid[0] is grid[1])  # they're the same list object

grid = [[0] * 3 for _ in range(3)]
grid[0][0] = 9
print(grid)                # only the first row changed

Output:

[[9, 0, 0], [9, 0, 0], [9, 0, 0]]
True
[[9, 0, 0], [0, 0, 0], [0, 0, 0]]
Command Prompt output showing a Python list of lists built with multiplication sharing one inner list, fixed with a comprehension
All three rows changed, and grid[0] is grid[1] prints True. The comprehension builds three separate lists.

The fix is the comprehension. It runs [0] * 3 once per row, so each row is a brand-new list.

Copying a nested list without surprises

list.copy() and slicing only copy the outer list. The rows inside are still shared, so editing one shows up in both. copy.deepcopy() copies all the way down:

import copy

prices = [["Seattle", 4.25], ["Miami", 3.95]]

shallow = prices.copy()
deep = copy.deepcopy(prices)

prices[0][1] = 4.75
print("original:", prices)
print("shallow: ", shallow)    # inner lists are shared, so it changed too
print("deep:    ", deep)       # a fully separate copy

Output:

original: [['Seattle', 4.75], ['Miami', 3.95]]
shallow:  [['Seattle', 4.75], ['Miami', 3.95]]
deep:     [['Seattle', 4.25], ['Miami', 3.95]]

Use a deep copy whenever you plan to edit the rows of the copy. Mutable vs immutable in Python explains why lists behave this way.

Sorting, columns and flattening a list of lists

Most built-ins work on nested lists once you tell them which column to use. A key function handles sorting, and zip(*rows) turns rows into columns:

from itertools import chain

sales = [
    ["Seattle", 1250.50, 980.25],
    ["Chicago", 2100.00, 1875.40],
    ["Miami", 875.75, 1320.10],
]

print(sorted(sales, key=lambda row: row[1], reverse=True)[0][0], "had the best Q1")
print([row[0] for row in sales])                     # one column
print(round(sum(row[1] for row in sales), 2))        # column total
print(list(zip(*sales))[1])                          # transpose: column 1 as a tuple

grid = [[1, 2, 3], [4, 5, 6]]
print(list(chain.from_iterable(grid)))               # flatten

Output:

Chicago had the best Q1
['Seattle', 'Chicago', 'Miami']
4226.25
(1250.5, 2100.0, 875.75)
[1, 2, 3, 4, 5, 6]
Command Prompt output sorting a Python list of lists by a column, pulling one column, totaling it, transposing with zip and flattening
Sort by Q1, grab the city column, total it, transpose and flatten, all in a few lines.

There’s a full walkthrough of sorting a list of lists and another on flattening a list of lists if you need more options.

List of lists vs NumPy array

List of listsNumPy 2D array
Mixed types (text and numbers)YesNot really, one dtype
Rows of different lengthsYesNo
Math on whole columnsLoop or comprehensionBuilt in and fast
Extra installNonepip install numpy

For a few hundred rows of mixed data, a list of lists is perfectly fine. Once you’re doing math on big grids of numbers, a 2D NumPy array is the better tool.

A few more list guides you might want next:

Frequently asked questions

What is a list of lists in Python?

It’s a list where each item is another list, like [[1, 2], [3, 4]]. People use it for tables, grids and matrices.

How do I access an element in a list of lists?

Use two indexes: data[row][column]. So data[1][0] is the first item in the second row.

How do I add a new row to a list of lists?

Call append() on the outer list with a list, for example data.append(["Miami", 875.75]).

Why does changing one row change all rows?

You probably built it with [[0] * n] * m, which repeats one inner list. Use [[0] * n for _ in range(m)] instead.

How do I copy a list of lists?

Use copy.deepcopy(data). A plain data.copy() or slice still shares the inner lists.

Can a list of lists have rows of different lengths?

Yes, Python doesn’t care. Each inner list is independent. The Python tutorial on nested list comprehensions shows more matrix examples.