runtime
85 / 250
lesson 85 Nested Data Structures · intermediate

Change nested data without surprises

`copy()` copies only the outer level. The inner lists and dictionaries are still shared, so changing them changes the original too. Copy every level you change.

You'll be able to
  • Explain why a copy of a dictionary still shares its inner dictionaries and lists
  • Return an updated record by copying the outer dictionary and each inner one you change
  • Spot the [[0] * 3] * 2 sharing trap in a grid
The idea

A function that "updates" a record usually shouldn't change the record it was given: the caller may still need the original, or may share it with other code. The Collection Algorithms module handled flat records with copy(). Nested records hide a trap: copy() makes a new outer dictionary, but the values inside it are the same inner dictionaries and lists, not copies of them.

Run each example to see what it prints. Running examples doesn't count toward progress.

example-1.py · A copy that still shares its inside
defaults = {"theme": "light", "alerts": {"email": True, "sms": False}}
mine = defaults.copy()
mine["theme"] = "dark"
mine["alerts"]["sms"] = True
print(defaults["theme"], defaults["alerts"]["sms"])
print(mine["alerts"] is defaults["alerts"])

The rule: copy every level you are about to change, from the top down to the value. Untouched inner parts can stay shared, because nobody changes them. Here the inner alerts dictionary is copied and changed, then a copy of the outer dictionary gets the new inner one.

example-2.py · Turn SMS on for one user only
def with_sms(settings, enabled):
    alerts = settings["alerts"].copy()
    alerts["sms"] = enabled
    updated = settings.copy()
    updated["alerts"] = alerts
    return updated

defaults = {"theme": "light", "alerts": {"email": True, "sms": False}}
user = with_sms(defaults, True)
print(user)
print(defaults)

Lists of lists have the same trap in its sneakiest form. [[0] * 3] * 2 doesn't build two rows: it builds one row and a list that mentions it twice. Build each row separately instead, for example with a comprehension: [[0] * 3 for _ in range(2)].

example-3.py · Two names for one row
grid = [[0] * 3] * 2
grid[0][0] = 9
print(grid)
safe = [[0] * 3 for _ in range(2)]
safe[0][0] = 9
print(safe)

When to use it: any function that returns a changed version of nested data (settings, orders with items, products with price tables) while the caller keeps the original. Mistakes to avoid: copying only the outer level and then changing something inside; appending to a list you got from the original record; and multiplying a list of lists.

Review
  • copy() copies one level; inner lists and dictionaries stay shared.
  • To change a nested value safely, copy each level on the path to it, top to bottom.
  • [[0] * n] * m repeats one row; build each row on its own.
  • Next module: when values can't be trusted, Python raises errors. The Exceptions & Validation module catches and raises them on purpose.
Practise this

Loading your progress…

To master this lesson, solve both challenges without hints or the answer, and pass the concept check.

Change nested data without surprises — Nested Data Structures · Runtime