PyDebug
← Back

PyDebug guides · 6 November 2026

Python List Aliasing: Copy vs Reference

One list can have several names. That is useful when a function is meant to update a shared collection, but it can also make a supposedly independent backup change unexpectedly. The key question is not “Do these variables have the same values?” but “Do they refer to the same mutable object?”

Assignment creates another reference

original = [1, 2]
alias = original
alias.append(3)
print(original)        # [1, 2, 3]
print(alias is original)  # True

Assignment does not copy the list. Both names point to the same object, and append changes that object in place. The is operator tests identity, while == compares values. Two separate lists can compare equal without being the same object. Do not use is to compare ordinary numeric or string values; use == for value equality.

A shallow copy separates the outer list

original = [1, 2]
copy = original.copy()  # also list(original) or original[:]
copy.append(3)
print(original)  # [1, 2]
print(copy)      # [1, 2, 3]
print(copy == original)  # False after the append

Here appending affects only the new outer list. A shallow copy copies the outer container, not all objects inside it. That distinction matters as soon as a list contains another mutable list.

Nested lists: follow both levels

rows = [[1, 2], [3, 4]]
shallow = rows.copy()
shallow[0].append(9)
print(rows)     # [[1, 2, 9], [3, 4]]
print(shallow)  # [[1, 2, 9], [3, 4]]
print(rows is shallow)        # False
print(rows[0] is shallow[0])  # True

The outer objects differ, but their first elements reference the same inner list. Replacing shallow[0] with a new list is different from mutating shallow[0]: replacement changes one outer container; an append changes the shared inner object.

When a deep copy helps

from copy import deepcopy
rows = [[1, 2], [3, 4]]
independent = deepcopy(rows)
independent[0].append(9)
print(rows)  # [[1, 2], [3, 4]]

deepcopy recursively copies the object graph, preserving internal sharing within the copied graph. It can be expensive and is not appropriate for every object, such as open files or objects with custom copy behaviour. If the structure is simple and known, copying only the mutable rows may be clearer: [row.copy() for row in rows].

Watch out for repeated inner lists

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

safe = [[0] * 3 for _ in range(2)]
safe[0][1] = 7
print(safe)  # [[0, 7, 0], [0, 0, 0]]

Multiplication repeats references to the same inner list. The comprehension creates a fresh row each time. This is a common source of bugs in grids, score tables and dynamic programming exercises.

Try this debugging checklist

  1. Write down which names point to which containers before the change.
  2. Mark each operation as mutation, such as append, or reassignment, such as x = x + [1].
  3. For nested data, check identity at each mutable level, not just the outer list.
  4. Test one mutation and assert that the object you intended to preserve is unchanged.

Practise the distinction in the shared list references exercise, then explore the lists and dictionaries learning path. Run your own variation in the Python playground.

← More Python guides