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
[[], [], []]

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]

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]]

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]

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 lists | NumPy 2D array | |
|---|---|---|
| Mixed types (text and numbers) | Yes | Not really, one dtype |
| Rows of different lengths | Yes | No |
| Math on whole columns | Loop or comprehension | Built in and fast |
| Extra install | None | pip 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:
- Create a list in Python
- Python list comprehension
- Get the index of an element in a list
- Dictionary of lists in Python
- Tuple of tuples in Python
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.

Bijay Kumar is a 13-time Microsoft MVP with more than 18 years in software development, and the founder of Python Guides and TSinfo Technologies. He started out building .NET and SharePoint solutions at HP, TCS and KPIT before moving into Python, machine learning and AI, and he also builds web apps with TypeScript and React. He writes the tutorials here himself, and every example is run before publishing so you see the real output. More about Bijay · Microsoft MVP profile · LinkedIn