Shallow copies, deep copies, and call by sharing
Why a copied list can still change under you, what deepcopy does that list() does not, the [[0] * 3] * 3 grid trap, and how arguments are really passed.
After this lesson you can
- Tell a shallow copy from a deep copy and predict which objects they share
- Explain why [[0] * 3] * 3 builds three references to one row
- Say how Python passes arguments, and what a function can and cannot change for its caller
Lesson 1 showed that assignment never copies: b = a gives one object
two names. Copying is a deliberate act, and it comes in two depths.
Shallow and deep
A shallow copy builds a new outer container and fills it with the
same inner objects. list(x), x[:], x.copy(), dict(d),
{**d} and copy.copy(x) are all shallow. For a flat list of numbers
or strings that is all you need; for a list of lists, or a dict of
lists, the inner containers are still shared, and mutating one through
the copy changes the original.
A deep copy, copy.deepcopy(x), copies recursively, so nothing
mutable is shared with the original. It keeps a memo of what it has
already copied: two references to one inner list become two references
to one new list in the copy, and a structure that contains itself
does not send it into an endless loop.
import copy def run(): config = {"name": "api", "ports": [80, 443]} shallow = config.copy() deep = copy.deepcopy(config) shallow["name"] = "worker" # rebinding a key: only the copy changes shallow["ports"].append(8080) # mutating a shared list: both change deep["ports"].append(9090) row = [1, 2] pair = [row, row] pair_copy = copy.deepcopy(pair) return { "config": config, "shallow": shallow, "deep": deep, "copy_rows_shared_with_each_other": pair_copy[0] is pair_copy[1], "copy_rows_shared_with_original": pair_copy[0] is row, }The grid trap, and += on a list
[0] * 3 is fine: it repeats references to an immutable 0. But
[[0] * 3] * 3 repeats a reference to one row list three times, so
writing one cell writes a column. Build each row separately:
[[0] * 3 for _ in range(3)].
+= on a list is extend: it mutates the list in place, so every
other name for it sees the change. x = x + [4] instead builds a new
list and rebinds only x. The same in-place step explains a famous
puzzle: t[0] += [2] on a tuple holding a list raises TypeError —
and the list has grown anyway, because the extend succeeded before the
tuple refused the assignment back into its slot.
def run(): bad = [[0] * 3] * 3 bad[0][0] = 1 good = [[0] * 3 for _ in range(3)] good[0][0] = 1 a = [1, 2] alias = a a += [3] # in place: alias sees it b = [1, 2] b_alias = b b = b + [3] # new list: b_alias does not t = ([1],) try: t[0] += [2] outcome = "no error" except TypeError as e: outcome = f"TypeError: {e}" return {"bad": bad, "good": good, "alias_after_iadd": alias, "alias_after_plus": b_alias, "tuple_error": outcome, "tuple_now": t}How arguments are passed
Python passes a reference to the object, by value — often called
call by sharing. Inside the function the parameter is a new name
for the caller's object. Mutating that object (items.append(...)) is
visible to the caller. Rebinding the parameter (items = [...]) only
changes what the local name points at, and the caller never sees it.
Neither "by value" nor "by reference" in the C++ sense describes it.
def add_item(items): items.append("added") def replace_items(items): items = ["replaced"] return items def run(): mine = ["start"] add_item(mine) returned = replace_items(mine) return {"mine": mine, "returned": returned}Common mistakes
- Taking
list(config)orconfig.copy()of nested data and then editing a nested part "in the copy". The original changes too. - Building a grid or a list of lists with
*, so every row is the same object. - Expecting a function to replace the caller's list by assigning to its parameter. Mutate it, or return the new one.
What interviewers ask
- What is the difference between
copy.copyandcopy.deepcopy? New outer object with shared contents against a full recursive copy; deepcopy's memo preserves shared references and handles cycles. - Is Python pass-by-value or pass-by-reference? Neither exactly: object references are passed by value, so mutation is shared and rebinding is not.
- What does
[[0] * 3] * 3produce? Three references to the same inner list; use a comprehension.
Exercise · Pro
Update a nested value without touching the original
Exercises are free in the first 3 lessons of every course. From here on they are part of Pro and Sprint; the lesson and its runnable examples stay open.
Upgrade