Skip to main content

Sharing, copying and nesting

Two names can share one list, so a change made through one shows through the other until a copy breaks the link. Tell a == b from a is b, see the same sharing in function calls and in a default argument, and copy the nested study table safely with copy.deepcopy.
P01.6.4L0 Foundation25 minIn review

Before this lesson

P01.6.3 Dictionaries and sets

After this lesson you can

  • Predict whether a change made through one name shows through another, by drawing the memory diagram.
  • Tell a == b (equal values) from a is b (one object), and make a separate copy of a list.
  • Explain why a function that changes a list it was given changes the caller’s list, and why a default argument written as [] is shared between calls.
  • Copy a list of dictionaries with copy.deepcopy where a shallow copy would still share the rows.
  • Sort the types met so far into those that can change (list, dict, set) and those that cannot (int, float, str, bool, tuple).

In review. The maths and code on this page are checked by automated tests on every change, but an independent reader has not signed it off yet. Spot a mistake or a confusing step? Tell us.

The question

You keep the scores in a, write b = a to keep a backup, add a bonus through b, and find that a has changed too. Python did nothing wrong. What did b = a really do, and how do you make a backup that stays put? The same question decides whether a function that cleans a dataset quietly changes the original as well.

From the last lesson

1. What does score_of.get('Femi', 0) give?

Show answer

0, the default, because 'Femi' is not a key. score_of['Femi'] would raise KeyError instead.

2. Why is 80 in score_of False?

Show answer

in on a dictionary checks the keys, and 80 is a value. 80 in score_of.values() is True.

Intuition

Lesson P01.2.2 drew assignment as an arrow from a name to an object: y = x points a second name at the object x names, and copies nothing. A list is an object like any other, so b = a does exactly the same. There is still one list, now with two names. Two names for one object are called aliases.

Two memory diagrams side by side. Left, headed b = a: the names a and b both have arrows to one blue list holding 55, 65 and 80. Underneath: a == b is True, same values; a is b is True, one object. Right, headed b = a.copy(): the name a points to a blue list 55, 65, 80 and the name b points to a second, sky-blue list with the same three values. Underneath: a == b is True, same values; a is b is False, two objects.
P01.6.4-F1

Left: b = a gives one list two names. Right: b = a.copy() makes a second list with the same values. == asks whether the values are equal; is asks whether there is only one object.

On the left, a change made through b is a change to the only list there is, so a shows it too. On the right there are two lists, and a change to one leaves the other alone. Both pictures pass the test a == b, because the values are equal. Only the left one passes a is b, which asks whether the two names point to one and the same object.

The sharing is only ever visible for objects that can change. An int or a string cannot be changed in place, so two names for one number never surprise you. Lists, dictionaries and sets can, and that is where this lesson lives.

Build it up

Step 1. A change through one name. b.append(40) changes the list that b names, which is the list that a names:

a = [55, 65, 80]
b = a
b.append(40)
print(a, b, a is b)
[55, 65, 80, 40] [55, 65, 80, 40] True

Step 2. A copy. a.copy() makes a new list with the same items, and so do list(a) and the slice a[:]. Afterwards the two lists are equal but not identical: == is True and is is False. A change to one leaves the other as it was.

c = a.copy()
print(c == a, c is a)
c.append(60)
print(a, c)
True False
[55, 65, 80, 40] [55, 65, 80, 40, 60]

Step 3. Equality against identity. x == y compares values, item by item for lists. x is y compares identities: each object has one, fixed for its lifetime, and id(x) shows it as a number. Use is only to ask “one object or two?”, as here, and never to compare numbers or strings: whether two equal numbers are one object or two is an accident of the interpreter.

Step 4. Passing a list to a function. A call binds each parameter to the object its argument names (P01.4.1). So inside the function the parameter is one more name for the caller’s list:

def add_bonus(values, bonus):
    for i in range(len(values)):
        values[i] = values[i] + bonus

scores = [55, 65, 80, 40, 60]
add_bonus(scores, 5)
print(scores)
[60, 70, 85, 45, 65]
A call-stack picture inside add_bonus(scores, 5), after its loop. At the top the global frame holds the orange name scores. Below it the add_bonus frame, outlined in sky blue, holds values, bonus and i. The arrows from scores and from values both reach one list, whose five slots hold 60, 70, 85, 45 and 65, all highlighted. The arrow from bonus reaches an int 5 and the arrow from i an int 4. A note reads one list, two names: scores and values, and below: values[i] = values[i] + bonus changed that one list, so print(scores) shows [60, 70, 85, 45, 65].
P01.6.4-F2

Inside the call the frame of add_bonus sits below the global frame, as in P01.4.3. Its name values points to the very list that scores names, so the loop changes the caller’s list.

Sometimes that is what you want. When it is not, build and return a new list, and leave the argument alone; the Code section does both.

Step 5. A default that is shared. A default value is worked out once, when def runs, not at every call. A default written as [] is therefore one list that every call shares:

def add_score(score, book=[]):
    book.append(score)
    return book

print(add_score(55))
print(add_score(65))

The second call prints [55, 65], not [65]: both calls appended to the same default list.

Step 6. Nesting. A container can hold containers. The study table as a list of dictionaries has one dictionary per row, and two steps of indexing reach one value: table[2] is Chen’s row and table[2]['score'] is 80. A copy of table made by table.copy() is a new outer list, but its slots point to the same five row dictionaries. The second pitfall below shows what that costs.

The formal statement

NoteDefinition: identity, mutability and copies
  • Every object has an identity, a type and a value. The identity never changes once the object is made. x is y is True exactly when x and y name the same object; x == y asks whether their values are equal.
  • An object is mutable if its value can change after it is made, and immutable if it cannot. Lists, dictionaries and sets are mutable; numbers, strings, bool values and tuples are immutable.
  • Assignment never copies: it binds a name to an existing object. Two names for one object are aliases.
  • A shallow copy of a container is a new container whose slots point to the same objects as the original’s (a.copy(), list(a), a[:], copy.copy(a)).
  • A deep copy (copy.deepcopy(a)) is a new container whose slots point to copies of those objects, made the same way all the way down. (Objects that can never change, such as numbers, may still be shared, which is safe.)

Two facts follow. When two names share one list, a is b is True and so is a == b, since both names show the same slots; but equal values never prove one object. And for a container of immutable objects, such as a list of numbers, a shallow copy is as safe as a deep one, because the shared items can never change.

Four panels in a two-by-two grid. List: the code [55, 65, 80] drawn as three numbered slots, with the note ordered, changeable. Tuple: ('Ama', 2, 55) as three numbered slots, ordered, fixed once made. Dict: {'Ama': 55, 'Ben': 65} as a two-column table, keys to values, changeable. Set: {'pass', 'fail'} as two words inside an oval, no order, no duplicates, changeable. Below the grid: can change once made, list, dict, set; cannot change, int, float, str, bool, tuple.
P01.6.4-F5

The four containers of this module side by side, and which types can change once made.

Go deeper: a tuple that holds a list

A tuple cannot change which objects it holds, but an object it holds may change. After t = ([55, 65], 'Ama'), the line t[0].append(80) works and t becomes ([55, 65, 80], 'Ama'), while t[0] = [1] is still a TypeError. The Python Language Reference puts it exactly: the value of an immutable container can change when a mutable object inside it changes, yet the container is still called immutable, because the collection of objects it holds cannot change. Such a tuple is also not hashable, so it cannot be a dictionary key.

Worked example

Three names, two lists. Trace this program, drawing the memory diagram after lines 3 and 5.

a = [55, 65, 80]
b = a
c = a.copy()
print(a == b, a is b, a == c, a is c)
b.append(40)
print(a, c)
Two memory diagrams side by side. Left, after line 3: the names a and b both point to one blue list 55, 65, 80, and the name c points to a second, sky-blue list 55, 65, 80. Under it: line 4 prints True True True False. Right, after line 5: a and b point to the blue list, which now holds 55, 65, 80 and a highlighted 40; c still points to the sky-blue list 55, 65, 80. Under it: line 6 prints [55, 65, 80, 40] [55, 65, 80].
P01.6.4-F3

The worked example as memory diagrams after line 3 and after line 5. The change made through b lands in the list that a names; c names a separate list.

Line 1 makes a list and points a at it. Line 2 points b at the same list; nothing is copied. Line 3 makes a second list with the same three values and points c at it.

Line 4 prints True True True False. a == b and a is b are both True: one list. a == c is True because the values are equal, but a is c is False: two lists.

Line 5 appends 40 to the list that b names. That is the list a names too, and nothing happens to the list of c.

Line 6 prints [55, 65, 80, 40] [55, 65, 80]. Now a == c would be False as well.

Code

The worked example, run:

a = [55, 65, 80]
b = a
c = a.copy()
print(a == b, a is b, a == c, a is c)
b.append(40)
print(a, c)
True True True False
[55, 65, 80, 40] [55, 65, 80]

When a function should not change its argument, it builds a new list and returns it. The caller then decides which name gets the result:

def with_bonus(values, bonus):
    result = []
    for v in values:
        result.append(v + bonus)
    return result

scores = [55, 65, 80, 40, 60]
curved = with_bonus(scores, 5)
print(scores, curved, curved is scores)
[55, 65, 80, 40, 60] [60, 70, 85, 45, 65] False

The shared default, and the usual repair: default to None, the single “nothing” object, and make a fresh list inside the call.

def add_score(score, book=[]):
    book.append(score)
    return book

def add_score_safely(score, book=None):
    if book is None:
        book = []
    book.append(score)
    return book

print(add_score(55), add_score(65))
print(add_score_safely(55), add_score_safely(65))
[55, 65] [55, 65]
[55] [65]

The first line prints [55, 65] [55, 65]: both calls returned the one default list, and print shows it after both appends. The nested study table, built with zip, which walks any number of lists in step:

import copy

names = ['Ama', 'Ben', 'Chen', 'Dina', 'Eli']
hours = [2, 4, 5, 1, 3]
table = []
for name, h, s in zip(names, hours, scores):
    table.append({'name': name, 'hours': h, 'score': s})
print(table[2], table[2]['score'])

shallow = table.copy()
deep = copy.deepcopy(table)
shallow[0]['score'] = 99
print(table[0]['score'], deep[0]['score'])
print(shallow is table, shallow[0] is table[0], deep[0] is table[0])
{'name': 'Chen', 'hours': 5, 'score': 80} 80
99 55
False True False

The shallow copy is a new outer list, but its row 0 is table’s row 0, so the change shows through table. The deep copy made its own rows and still says 55.

Explore

P01.6-X1

Box-diagram workbench

Change the list with the buttons, then make b with b = a and change the list again through either name: whose slots move? Then press b = a.copy() and repeat. The read-out gives a == b and a is b after every step.

Two memory diagrams side by side. Left, b = a: the names a and b both point to one list holding 55, 65 and 80, and a is b is True. Right, b = a.copy(): a and b point to two separate lists with the same values, and a is b is False.

Pitfalls

Two panels side by side. Left, marked with a cross and headed shallow = table.copy(): an outer list named table at the top and an outer list named shallow at the bottom, sky blue, both with five slots whose arrows reach the same five row boxes in the middle, Ama, Ben, Chen, Dina and Eli with their scores; Ama's row is highlighted and reads 99. Under it: shallow[0]['score'] = 99 changes Ama's row in table too. Right, marked with a tick and headed deep = copy.deepcopy(table): the slots of table reach one row of five boxes with the original scores, and the slots of deep reach a second, sky-blue row of five boxes, where only Ama's reads 99. Under it: deep[0]['score'] = 99 changes the copy's row only.
P01.6.4-F4

Wrong beside right. Left: a shallow copy is a new outer list whose slots reach the same rows, so a change to a row shows through both. Right: a deep copy has rows of its own.

“b = a makes a backup of a.”

It makes a second name for the same list, so the “backup” changes whenever the list does. Make a copy: b = a.copy(), list(a) or a[:].

“table.copy() copies everything inside it.”

It copies one level only. The new outer list holds arrows to the same row dictionaries, so changing a row through the copy changes the table, as on the left of the figure. When the items can change, use copy.deepcopy. In machine learning this is how a “copy” of a dataset made before cleaning ends up cleaned as well.

“a == b, so a and b are the same list.”

== only says the values are equal. Two separate lists can be equal now and differ after the next append. Whether they are one list is the question a is b answers.

“A default of [] gives every call a fresh empty list.”

The default is made once, when def runs, and every call that leaves out the argument shares it. Write book=None and make the list inside the function, as in the Code section.

“I can remove items from a list while I loop over it.”

The loop keeps its own count of positions, and every removal moves the later items one place to the left, so the loop skips an item. With values = [40, 45, 70], removing every item below 50 inside for v in values: leaves [45, 70]. Loop over a copy instead, for v in values.copy():, or build a new list of the items to keep.

Check yourself

Recall

1. After a = [1, 2], b = a and b.append(3), what is a?

Show answer

[1, 2, 3]. a and b are two names for one list.

2. After a = [1, 2] and c = a.copy(), what are a == c and a is c?

Show answer

a == c is True (equal values) and a is c is False (two lists).

3. Which of list, tuple, dict, set and str can change once made?

Show answer

list, dict and set. A tuple and a str cannot.

4. Why does a function def add(x, book=[]) keep items from earlier calls?

Show answer

The default list is made once, when def runs, and every call that leaves out book appends to that same list.

5. What is the difference between table.copy() and copy.deepcopy(table) for a list of dictionaries?

Show answer

table.copy() is a new outer list whose slots point to the same row dictionaries. deepcopy also copies every row, so nothing is shared.

Exercises

1. ★ Predict the output, then draw the memory diagram.

a = [55, 65]
b = a
c = a[:]
a.append(80)
print(a, b, c)
print(a == b, a is b, a == c, a is c)
Show solution

b is a second name for the list of a, and c is a new list made by the slice. The append changes the one list that a and b share. The first print shows [55, 65, 80] [55, 65, 80] [55, 65] and the second shows True True False False.

2. ★★ This function is meant to return the scores of at least 50 and leave its argument unchanged. Find both bugs and repair it.

def passes(values):
    for v in values:
        if v < 50:
            values.remove(v)
    return values
Show solution

It changes the caller’s list, because values is a second name for it, and it removes items while looping over the same list, which skips items: passes([40, 45, 70]) returns [45, 70]. Build a new list instead:

def passes(values):
    kept = []
    for v in values:
        if v >= 50:
            kept.append(v)
    return kept

Now passes([40, 45, 70]) is [70], passes([55, 65, 80, 40, 60]) is [55, 65, 80, 60], and the argument is unchanged.

3. ★★★ Predict rows and shallow at the end, then repeat with copy.deepcopy(rows) in place of rows.copy().

rows = [[55, 65], [80, 40]]
shallow = rows.copy()
shallow[0][0] = 99
shallow.append([60])
Show solution

shallow is a new outer list whose two slots point to the same inner lists as rows. So shallow[0][0] = 99 changes the inner list both share, while append changes only the new outer list. At the end rows is [[99, 65], [80, 40]] and shallow is [[99, 65], [80, 40], [60]].

With copy.deepcopy(rows) the inner lists are copied too, so the change to [0][0] lands in the copy only: rows stays [[55, 65], [80, 40]] and the copy is [[99, 65], [80, 40], [60]].

Summary card

Names point to objects; only a copy makes a second object.

The anchor figure again: on the left, b = a, two names with arrows to one list; on the right, b = a.copy(), two names with arrows to two lists of the same values.
P01.6.4-F1

One list with two names, or two lists.

  • b = a shares; a.copy(), list(a) and a[:] copy one level; copy.deepcopy(a) copies all the way down.
  • a == b asks about values; a is b asks about one object.
  • A list passed to a function, or used as a default, is shared, not copied.
  • Can change: list, dict, set. Cannot: int, float, str, bool, tuple.

Sources and next steps

Sources. Each was opened and checked against this lesson on 12 November 2026.

  • Python Software Foundation, The Python Language Reference, section 3.1, Objects, values and types: every object has an identity, a type and a value; is compares identities; mutable and immutable objects, with lists and dictionaries mutable and numbers, strings and tuples immutable; and the note on an immutable container that holds a mutable object. After c = []; d = [] the two names are guaranteed to refer to two different lists.
  • Python Software Foundation, The Python Tutorial, section 3.1.3: simple assignment never copies data, and a change made through one variable is seen through every other that refers to the same list. Section 4.9.1, Default Argument Values: the default is evaluated only once, with the example f(a, L=[]) and the repair L=None.
  • Python Software Foundation, copy: Shallow and deep copy operations: assignment creates bindings, not copies; a shallow copy inserts references to the objects of the original and a deep copy inserts copies of them, recursively.
  • Allen B. Downey, Think Python, 3rd edition, chapter 9, sections 9.9 (objects and values, the is operator), 9.10 (aliasing) and 9.11 (list arguments: a function given a list gets a reference to it). Free under CC BY-NC-SA 4.0.
  • Ana Bell, MIT OpenCourseWare 6.100L, Lecture 11: Aliasing, Cloning, Fall 2022, lecture slides: slide 2 (L[:] copies the top level only), slides 13 to 18 (removing items while looping over the same list, and looping over a clone), slide 26 (= makes an alias, not a clone), slide 28 (passing a list to a function makes an alias) and slides 31 to 39 (shallow and deep copies with copy.copy and copy.deepcopy).

Next. P01.7, Strings and files, listed on the course page, treats text as a sequence that indexes and slices like a list but, like a tuple, can never change.

This lesson unlocks

P01.7 Strings and files

Your progress is saved in this browser only. No account, no tracking.
Back to top