Back to Blog
Python

Python List of Lists: Creation, Indexing, and Copying

python list of lists: Learn how to create, index, modify, and copy lists of lists in Python, including shared-reference bugs and when to choose a different structure.

nested listslist comprehensionshallow copydeep copydata structures
Diagram showing a Python list whose elements are nested lists, illustrating a two-dimensional grid structure.

A python list of lists is a list where each element is itself a list. It is the standard way to represent tabular data, grids, matrices, or grouped values without importing a library. The outer list holds references to the inner lists, and that reference behavior drives most of the surprising edge cases you will run into.

Creating a List of Lists

The most direct form is a literal:

matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

Each inner list is a separate object. You can also build one programmatically with a list comprehension:

rows = [[0] * 3 for _ in range(4)]

This creates four independent rows, each containing three zeros. The expression [0] * 3 repeats the integer zero, which is safe because integers are immutable. The comprehension runs once per iteration, so each inner list is a distinct object.

A common mistake is [[0] * 3] * 4. The outer multiplication repeats the same inner list object four times. The result is four references to one list, so modifying any row changes every row. This is the single most frequent bug when working with nested lists.

Indexing Nested Lists

Accessing an element requires two indices: matrix[row][col]. The first index selects the inner list, the second selects the element inside it.

grid = [[1, 2], [3, 4]] print(grid[0][1]) # 2

Negative indices work at both levels, so grid[-1][-1] returns the last element of the last inner list. Slicing also applies at each level, but the semantics differ. Slicing an inner list returns a new list with copied element references. Slicing the outer list returns a new list that still references the same inner list objects.

Modifying Nested Lists

You can replace an entire inner list by assigning to an outer index:

grid[0] = [9, 9]

Or modify a single element inside a row:

grid[0][1] = 8

The distinction matters when you pass a list of lists into a function. Reassigning grid[0] inside the function changes the outer list, but the original inner list object is untouched. Modifying grid[0][1] mutates the shared inner list, so the change is visible to every reference that holds that list. If the function receives the list as a parameter, both operations affect the caller's data because the outer list itself is shared.

Copying Nested Lists

list.copy() and copy.copy only copy the outer list. The inner lists remain shared between the original and the copy. copy.deepcopy creates new inner lists as well.

import copy original = [[1, 2], [3, 4]] shallow = original.copy() shallow[0][0] = 99 print(original[0][0]) # 99 deep = copy.deepcopy(original) deep[0][0] = 1 print(original[0][0]) # still 99

Deep copy gives full isolation but costs more time and memory because every nested object is duplicated. For a small grid in a script, the cost is negligible. For a large matrix processed frequently, avoid copying entirely and mutate in place when the data is not shared.

Iterating and Flattening

A nested loop is the straightforward way to visit every element:

for row in matrix: for value in row: print(value)

Flattening with a comprehension:

flat = [value for row in matrix for value in row]

The order of the for clauses follows the nesting depth. Reading left to right: pick each row, then each value in that row. The same ordering applies if you add a conditional filter at the end.

Memory and Performance Considerations

A list of lists stores references, not contiguous values. Each inner list carries its own allocation and bookkeeping overhead, so a large grid uses more memory than a flat list holding the same number of elements. Accessing matrix[row][col] also involves an extra level of indirection compared to indexing a flat list.

For numerical work, a flat list or a numpy array is usually the better choice. NumPy stores values contiguously and supports vectorized operations. A list of lists is appropriate when the data is heterogeneous, the rows have irregular lengths, or you want to avoid adding a dependency.

When a List of Lists Is the Wrong Choice

If rows have different lengths, a list of lists still works, but column access becomes uneven and code that assumes a rectangular shape will fail. If you need fast column access, a list of lists is awkward because columns are not stored contiguously. A list of dictionaries or a list of dataclass instances is often clearer when each row represents a record with named fields, because field access is explicit and the structure is self-documenting.

python list of lists: Practical Usage and Code Examples | RYUSLOG DEV