Assignment does not copy¶
Level: 201 · for Python programmers, especially ones coming from C or ABAP
One line: b = a gives the object a second name and copies nothing, so a change made through either name shows through both — which is why [[0] * 3] * 3 is one row three times, why a copy made with [:] still shares its inner lists, and why a function can change the list you passed it.
A name in Python is an entry in a namespace that points at an object. It has no type, no size and no storage of its own; the object has all three. So b = a cannot copy anything, because there is nothing on the left to copy into: it writes a second entry pointing at the same object. C copies the bytes of a struct on =, and ABAP copies the value of an internal table. Python has no version of that assignment at all. Every =, every argument passed to a function and every element put into a container is the pointer case.
Most of the time nothing shows, and that is why the wrong model survives so long. An int, a str, a tuple or a bytes object cannot change after it is made, so two names for one of them are indistinguishable from two copies. The rule only becomes visible when the shared object is a list, a dict, a set, a bytearray or an instance of a class you wrote, and someone changes it through one of its names.
The run¶
Verified output of assignment_does_not_copy_py.py — regenerated by tools/run_examples.py, never hand-typed.
1. TWO NAMES, ONE LIST
a = [1, 2]
b = a
b.append(3)
a [1, 2, 3] the change made through b is visible through a
a is b True because there is one list, with two names
2. REBINDING IS NOT MUTATING
b = b + [4]
a [1, 2, 3] unchanged: + built a new list
b [1, 2, 3, 4] and b now names that new list
a is b False
n = 1
m = n
m += 1
n 1 an int cannot change, so two names never trap you
3. THE GRID THAT IS ONE ROW, THREE TIMES
grid = [[0] * 3] * 3
grid[0][0] = 1
grid [[1, 0, 0], [1, 0, 0], [1, 0, 0]] every row changed
grid[0] is grid[1] True * repeated the reference, not the row
rows = [[0] * 3 for _ in range(3)]
rows[0][0] = 1
rows [[1, 0, 0], [0, 0, 0], [0, 0, 0]] the comprehension ran [0] * 3 three times
rows[0] is rows[1] False
4. A COPY IS ONE LEVEL DEEP
a = [[1], [2]]
b = a[:]
a is b False the outer list is new
a[0] is b[0] True the inner lists are the same ones
b[0].append(9)
a [[1, 9], [2]] so a changed through the copy
Every way of copying a list is shallow, except one:
a[:][0] is a[0] True
list(a)[0] is a[0] True
a.copy()[0] is a[0] True
copy.copy(a)[0] is a[0] True
sorted(a)[0] is a[0] True
(a * 2)[0] is a[0] True
[*a][0] is a[0] True
copy.deepcopy(a)[0] is a[0] False deepcopy is the one that copies the inside
d = {'k': [1]}
e = dict(d)
e['k'].append(2)
d {'k': [1, 2]} dict(d) and d.copy() are shallow too
5. A CALL IS AN ASSIGNMENT
def grow(lst): lst.append(0)
def regrow(lst): lst = lst + [0]
data = [1]
grow(data)
data [1, 0] the parameter named the caller's list, and mutated it
regrow(data)
data [1, 0] the parameter was rebound; the caller's name was not
6. AN IMMUTABLE CONTAINER, WITH MUTABLE CONTENTS
t = ([], 'x')
t[0].append(1)
t ([1], 'x') the tuple did not change: it holds the same list, which did
t[1] = 'y' TypeError
hash(t) TypeError and a tuple holding a list cannot be a dict key
What the run shows¶
Section 1: two names, one list. b.append(3) changes the list, and a names that same list, so a shows the change. a is b is True because there is one object. Nothing was copied at b = a, and nothing could have been.
Section 2: rebinding is not mutating. b = b + [4] looks like the same kind of statement as b.append(3), and it is the opposite: + builds a new list, and = points b at it. a keeps the old list and a is b is now False. The four int rows are why people meet this rule late. m += 1 cannot change the object 1, so it rebinds m to a new int, and n is untouched; with objects that cannot change, sharing has no consequences. += is not + is about the same statement when the object can change.
Section 3: the grid that is one row, three times. [[0] * 3] * 3 runs the inner [0] * 3 once, which makes one list, and then the outer * 3 repeats the reference to it. grid[0] is grid[1] is True, so setting grid[0][0] sets the only row there is. The comprehension runs [0] * 3 on each of its three iterations, so rows holds three lists. The same trap is in [[]] * n, [{}] * n and dict.fromkeys(keys, []), which gives every key the one list.
Section 4: a copy is one level deep. a[:] builds a new outer list whose elements are the very same inner lists: a is b is False and a[0] is b[0] is True. That is a shallow copy, and the table shows that every idiom for copying a list makes one: a[:], list(a), a.copy(), copy.copy(a), sorted(a), a * 2, [*a]. Only copy.deepcopy copies what the elements point at. dict(d) and d.copy() are shallow in the same way. A shallow copy is a complete copy whenever the elements are objects that cannot change, which is the usual case: a list of numbers or strings. It is the nested list and the dict of lists where the inside is still shared.
Section 5: a call is an assignment. Passing data to grow binds the parameter name lst to the caller's list, the way b = a did in section 1. lst.append(0) therefore changes the caller's list. regrow builds a new list and binds its local name to it, and the caller sees nothing, because rebinding a name never affects the other names of the object it used to point at. Python's convention is neither pass-by-value, since the list was not copied, nor pass-by-reference, since regrow could not rebind the caller's data. A function that means to change the caller's list mutates it; one that means not to copies first.
Section 6: an immutable container, with mutable contents. t[1] = 'y' raises TypeError, because a tuple's slots cannot be reassigned. t[0].append(1) succeeds, because it changes the list, not the tuple: t still holds the same list, which now has one element. That is also why hash(t) raises. A tuple is hashable only when every element is, and a list is not, so a tuple with a list inside cannot be a dictionary key or a set member.
If you are coming from C¶
b = a on two structs copies every byte, and on two pointers copies the address. Python names are always the second kind, and there is no first kind: no declaration puts a list "in" b, so no assignment can copy one there. Asking for a copy is a call, list(a) or copy.deepcopy(a), and section 4 says how deep each one goes. There is also no & to warn you that sharing is about to start. It starts at every assignment, every call and every insertion into a container, and the only way to know whether it matters is to know whether the object can change.
If you are coming from Rust¶
Rust decides the same question by type. let b = a; copies a Copy type such as an i32 and moves anything else, after which a is gone and using it is E0382. Two live names for one heap object take Rc or Arc, and changing it through them takes RefCell or a Mutex, so the compiler always knows who may write. Python has only the shared case and no compiler tracking it: the discipline the borrow checker enforces is yours to keep. The Rust library's Copy or move ↗ is the fork Python does not have, and A name is not a place ↗ is the half of the model the two languages share.
If you are coming from ABAP¶
ABAP assignment is by value. lt_b = lt_a gives lt_b its own internal table, and a later APPEND to one is invisible through the other; the runtime may share the storage until the first write, but that is an optimisation that keeps the value semantics, not a change to them. Sharing has to be asked for, with a data reference (REF TO), a field symbol (ASSIGN) or a CHANGING parameter, and an IMPORTING parameter cannot be written to at all. So ABAP has both kinds of assignment and chooses at the declaration. Python has one kind, a function may change any argument it receives, and whether that matters depends on the object rather than on the signature. (Not machine-checked — CI cannot run ABAP.)
Try it¶
- Search a codebase you maintain for a list literal multiplied by a count:
grep -rn '\] \* ' --include='*.py'. For each hit, ask what the repeated element is.[0] * nis fine;[[]] * nand[{}] * nare section 3. - Take a function of yours that receives a list or a dict, call it with
copy.deepcopy(arg)as well as witharg, and compare the two arguments afterwards with==. If they differ, the function mutates its input, and every caller shares that. - Find a
.copy()or[:]in your code whose elements are lists or dicts. Change one element through the copy and print the original. Section 4 says which idiom would have protected it. - Run
d = dict.fromkeys(['a', 'b'], [])and append tod['a']. Write down whatd['b']will be before you look. - Look for a function that returns a list or dict it also keeps, such as a cache or a module-level default. A caller that appends to the result is appending to your cache.
See also¶
+=is not+: the statement that mutates when it can and rebinds when it cannotbytearrayis the mutable one: the same split between a shared change and a rebinding, on the two binary types, with a kata about exactly this- Slicing is not indexing: what
a[:]is, and why it never raises - Copy or move ↗ and A name is not a place ↗ in the Rust library
- Objects, values and types ↗ in the language reference, and the
copymodule ↗ - Two entries in the Python FAQ: How do I create a multidimensional list? ↗ and How do I write a function with output parameters? ↗