This lesson separates objects from values, introduces the is operator, explains aliasing and why it is dangerous only for mutable objects, shows that a function can modify its caller's list, and gives the rules for avoiding the resulting bugs.
Subject: Python · 65 slides · code lesson
Open the interactive version of this deck
Title
Python · Chapter 10 — Lists
§10.10-10.13, pp. 95-99
Objectives
Five things, each one you can check yourself at an interpreter prompt.
Think Python, 2nd edition — Allen B. Downey §10.10-10.13, pp. 95-99 — the pages these objectives are drawn from
Warm-up
Two lists with the same elements. Ask whether that makes them one thing.
Discussion prompt
You write a = [1, 2, 3] and then b = [1, 2, 3]. Are a and b the same list, or two lists that happen to look alike? Does the difference matter, and how would you find out?
Hint: Try changing one of them.
Answer:
They are two separate lists. Changing one leaves the other alone, which is how you could find out.
Whether it matters depends entirely on whether they can be changed — and lists can. For two strings with the same characters, the question would be academic.
Python has an operator for asking directly, and this lesson is about what it tells you and why the answer matters so much for mutable types.
Concept
Until now we have been using object and value interchangeably, but it is more precise to say that an object HAS a value. If another list has the same elements, we say it has the same value — but it is not the same object.
object — Something a variable can refer to. An object has a type and a value.
Two objects with the same value are equivalent. Two names for one object are identical. If two objects are identical they are also equivalent, but if they are equivalent they are not necessarily identical.
Figure (svg): Two columns contrasting two equivalent objects with two names for one identical object
Think Python, 2nd edition — Allen B. Downey §10.10-10.13, pp. 95-96
Section
Section 1
Concept
To check whether two variables refer to the same object, you can use the is operator. It asks a different question from the equality operator, and the two can disagree.
>>> a = 'banana'
>>> b = 'banana'
>>> a is b
True
>>> a = [1, 2, 3]
>>> b = [1, 2, 3]
>>> a is b
False| Case | What happened | Note |
|---|---|---|
| two strings | Python created only one string object | both names refer to it |
| two lists | two separate list objects | with equal values |
| is | asks about identity, not equality | the two questions differ |
In the string example, Python only created one object and both names refer to it. When you create two lists, you get two objects — which is the book's figure 10.3, and it is why the string case is a curiosity and the list case is not.
Think Python, 2nd edition — Allen B. Downey §10.10-10.13, pp. 95-96
Picture it
For the strings there are two possible states and it does not matter which. For the lists there is only one.
Figure (svg): A state diagram showing a and b pointing at two separate but equal lists
Notice there are two boxes. If a change is made to one, the other is unaffected — which is why this diagram is worth drawing rather than assuming.
Worked example
Two operators, two questions, and they can disagree.
>>> a = [1, 2, 3]
>>> b = [1, 2, 3]
>>> a == b
True
>>> a is b
False| Expression | The question it asks | Answer |
|---|---|---|
| a == b | do they have the same value? | True: same elements |
| a is b | are they the same object? | False: two objects |
| the relationship | identical implies equivalent, not the reverse | one-way |
Ask about value.
Why: The equality operator compares contents, and these two lists have the same elements — so they are equivalent.
Ask about identity.
Why: The is operator asks whether the two names refer to the same object, and here they do not.
State the relationship precisely.
Why: If two objects are identical, they are also equivalent; but if they are equivalent, they are not necessarily identical.
Figure (svg): The state of the program after each line of Worked example is and asking different questions, drawn as a ladder with one rung per traced line
True and False. Equality is about contents and identity is about which object — and only one of the two implications holds between them.
Verify: Check the implication in the direction that does hold.
Why: With b = a, both a == b and a is b are True — identity implies equivalence. Finding no case where is is True and == is False confirms the one-way relationship, and it is why is is the stronger claim.
Prediction
Two lists with the same elements.
>>> a = [1, 2, 3]
>>> b = [1, 2, 3]
>>> a is b| Step | The question | Answer |
|---|---|---|
| two list literals | two separate objects created | each with its own box |
| a is b | are they the same object? | no |
| a == b | do they have the same value? | yes |
Predict first
What does a is b report?
Correct: False — when you create two lists, you get two objects, however similar their contents.
Why: Each list literal creates a new list object, so a and b refer to two different objects that happen to be equivalent. a == b would be True, because equality compares contents. The two operators ask different questions, and this is the case where they disagree.
Worked example
Python created one string and two lists. The reason is immutability.
>>> a = 'banana'
>>> b = 'banana'
>>> a is b
True
# but this is not guaranteed, and it does not matter:
# nothing can change either string| Aspect | What is true | Note |
|---|---|---|
| the strings | Python may share one object | an optimisation |
| why it is safe | strings are immutable | no one can change it |
| why it does not matter | sharing is undetectable | except through is |
Notice Python shared the string.
Why: Only one string object was created, and both names refer to it.
See why that is safe.
Why: Strings are immutable, so nothing either name does can change the shared object. The sharing is invisible.
See why it is not done for lists.
Why: A shared list could be modified through one name and the change would be visible through the other, which would be a surprise rather than an optimisation.
Figure (svg): Two columns contrasting why sharing is safe for immutable objects and dangerous for mutable ones
Python shares immutable objects freely because sharing cannot be detected. For mutable objects it does not, because sharing would be visible the moment one of them changed.
Verify: Check whether the string sharing is something to rely on.
Why: It is not: whether two equal strings are the same object depends on how they were created, and Python makes no promise. The book's own conclusion is that it almost never makes a difference whether a and b refer to the same string or not — which is exactly why immutability makes the question uninteresting.
Trap
A student writes if x is 1000: to test whether a number equals a thousand.
Read is as a more emphatic equals
Why: It reads like English — if x is a thousand — which is exactly the meaning of ==.
is asks whether they are the same object, and two separately created numbers need not be. The test can be False for a value that is genuinely a thousand, and it may behave differently for small numbers, which Python happens to share.
Use == for values and is for identity.
Ask what you actually want to know
Why: Almost always: do these have the same value? That is ==
Reserve is for identity questions
Why: Chiefly is this the same object — and the one common case, x is None, where identity really is the question.
The reason is is misleading is that it works for small integers, which Python shares, and fails for larger ones — so a test written with is can pass every small case and fail in production.
Discrimination
Ask whether you care about identity or about contents.
Sort into buckets
For each question, which operator answers it?
Two truths and a lie
Two are true. Keep the lie.
Eliminate the wrong options
Rule out the two true statements.
Survives elimination: C
Why: C reverses the implication, and the reversal is false: a = [1,2,3] and b = [1,2,3] are equivalent and not identical. The book states the relationship precisely — if two objects are identical they are also equivalent, but if they are equivalent they are not necessarily identical. Only one direction holds.
Socratic
The answer explains why the whole distinction matters.
Discussion prompt
Python may create one object for two equal strings, and always creates two for two equal lists. Explain why, in terms of what could be detected.
Hint: How would you tell that two strings were secretly the same object?
Answer:
You could not, except with the is operator. Nothing can change a string, so two names referring to one string behave exactly like two names referring to two — the sharing is undetectable and therefore free.
For lists it would be detectable immediately: modify one, and the other changes. That is not an optimisation, it is a change in behaviour.
So immutability is what makes sharing safe, and mutability is what makes the identity question worth asking. That is the connection between this section and the next: aliasing is only a problem for objects that can change.
Section
Section 2
Concept
If a refers to an object and you assign b = a, then both variables refer to the same object. The association of a variable with an object is called a reference, and an object with more than one reference is aliased.
aliasing — A circumstance where two or more variables refer to the same object.
>>> a = [1, 2, 3]
>>> b = a
>>> b is a
True
>>> b[0] = 42
>>> a
[42, 2, 3]| Line | What happens | Note |
|---|---|---|
| b = a | copies the reference, not the list | one object, two names |
| b[0] = 42 | modifies the object | through b |
| a | sees the change | because it is the same object |
If the aliased object is mutable, changes made with one alias affect the other. Although this behaviour can be useful, it is error-prone — in general it is safer to avoid aliasing when you are working with mutable objects.
Think Python, 2nd edition — Allen B. Downey §10.10-10.13, pp. 96-97
Picture it
This is the picture that explains everything else in the lesson.
Figure (svg): A state diagram showing two names a and b both pointing at a single list object
Compare it with figure 10.3 from the previous section, which had two boxes. The pictures are the difference, and drawing them is the fastest way to predict what a program will do.
Worked example
Nothing mentions a, and a changes.
>>> a = [1, 2, 3]
>>> b = a
>>> b[0] = 42
>>> a
[42, 2, 3]
>>> b
[42, 2, 3]| Line | What happens | Effect on a |
|---|---|---|
| b = a | b refers to the same object | no copy is made |
| b[0] = 42 | the OBJECT is modified | not the name |
| a | refers to that object | so it sees the change |
Notice what the assignment copied.
Why: b = a copies the reference — the arrow — not the list. Only one list object exists.
Notice what the modification changed.
Why: The bracket assignment changed the object, not either name. Both names still point where they pointed.
Conclude why a changed.
Why: It did not change: it refers to the same object it always did, and that object now holds something different.
Figure (svg): The state of the program after each line of Worked example a change through one alias, drawn as a ladder with one rung per traced line
Both a and b show [42, 2, 3], because there is one list and both names refer to it. Nothing about a was modified — the object it points at was.
Verify: Compare with the same code on strings.
Why: With a = 'abc' and b = a, there is no way to modify the string, so the aliasing has no observable consequence at all. That is why the book says aliasing is not as much of a problem for immutable objects — the danger comes entirely from mutability.
Prediction
One assignment, and then a modification through the other name.
a = [1, 2, 3]
b = a
b.append(4)
print(a)| Line | What happens | Result |
|---|---|---|
| b = a | one object, two names | aliased |
| b.append(4) | modifies the shared object | in place |
| print(a) | the same object | [1, 2, 3, 4] |
Predict first
What does this print?
Correct: [1, 2, 3, 4] — a and b refer to the same list, and append modified it in place.
Why: b = a copied the reference rather than the list, so there is one object with two names. append is a method that modifies, so the change is visible through both. Note that b = b + [4] would have behaved completely differently: it would create a new list and repoint b, leaving a unchanged.
Worked example
Two lines that look similar and do completely different things.
>>> a = [1, 2, 3]
>>> b = a
>>> b = [9, 9, 9] # reassignment
>>> a
[1, 2, 3]
>>> b = a
>>> b[0] = 9 # modification
>>> a
[9, 2, 3]| Line | What it does | Effect on a |
|---|---|---|
| b = [9, 9, 9] | repoints b at a NEW object | a is unaffected |
| b[0] = 9 | modifies the SHARED object | a sees it |
| the difference | one moves an arrow, one changes a box | and only one is visible through a |
Read the first as an arrow moving.
Why: b = [9, 9, 9] creates a new list and points b at it. The old list is untouched and a still refers to it.
Read the second as a box changing.
Why: b[0] = 9 leaves both arrows where they are and changes what is in the box they both point at.
State the rule.
Why: Reassignment affects one name; modification affects every name that refers to the object.
Figure (svg): Two columns contrasting reassigning a name with modifying the shared object
The reassignment leaves a alone; the modification changes what a sees. The two lines differ by the presence of the bracket, and the consequences are entirely different.
Verify: Check with is after each line.
Why: After the reassignment, a is b is False — they are separate objects again. After the modification, a is b is True throughout. The is operator makes the difference between the two lines observable rather than something you have to reason about.
Trap
A student writes backup = original before modifying original, expecting to have kept a copy.
Read assignment as copying
Why: It copies for numbers and strings in every way that can be observed, so the habit is well founded.
It copies the reference, not the object. Both names now refer to one list, and modifying it destroys the supposed backup along with the original.
Make an actual copy with a slice.
backup = original[:] creates a new list
Why: Which is why lesson 10a noted that t[:] is a curiosity for strings and a technique for lists.
Verify with is if you are unsure
Why: backup is original should be False if a real copy was made.
The book's third piece of debugging advice is exactly this: make copies to avoid aliasing. If you want to use a method like sort that modifies the argument but you need to keep the original as well, you can make a copy.
Invariant
Step through and watch the arrows.
Step through it
After the fourth line, how many list objects exist, and what does each name refer to?
Two objects: a refers to [42, 2, 3] and b to [7, 8]. The third line changed a box and the fourth moved an arrow, and only the first of those was visible through a.
Faded example
Assignment copies the reference. This copies the list.
Fill in the blanks
original = [3, 1, 2]
backup = original[:]
backup.sort()
# original should still be [3, 1, 2]
Why: A slice with both indices omitted produces a new list containing the same elements, so backup refers to a different object and sorting it leaves original alone. Writing backup = original would alias rather than copy, and the sort would reorder the original too. The book gives this exact example, and also mentions the built-in sorted, which returns a new sorted list without needing the copy at all.
Explain it
The commonest aliasing surprise, and it has a one-picture explanation.
Discussion prompt
A classmate saved a backup with backup = original, sorted the original, and found the backup sorted too. Draw them the picture and give them the fix.
Hint: Count the boxes.
Answer:
Draw one box with two arrows pointing at it. Say: the assignment copied the arrow, not the box — there is only one list, and it has two names.
So sorting through either name sorts the one list, and both names show the sorted result. Nothing was copied at any point.
The fix is backup = original[:], which creates a second box. Then there are two lists and sorting one leaves the other alone — and they can check it with backup is original, which should be False.
Section
Section 3
Concept
When you pass a list to a function, the function gets a reference to the list. If the function modifies the list, the caller sees the change.
def delete_head(t):
del t[0]
>>> letters = ['a', 'b', 'c']
>>> delete_head(letters)
>>> letters
['b', 'c']| Part | What happens | Note |
|---|---|---|
| the call | passes a reference | not a copy |
| t and letters | aliases for the same object | two frames, one list |
| del t[0] | modifies the shared object | letters sees it |
The parameter t and the variable letters are aliases for the same object. Since the list is shared by two frames, the book's figure 10.5 draws it between them rather than inside either.
Think Python, 2nd edition — Allen B. Downey §10.10-10.13, pp. 97-97
Picture it
The list belongs to neither frame, which is exactly the point.
Figure (svg): A stack diagram showing main with letters and delete_head with t, both referring to one shared list
This is genuinely new. Every function before this chapter received immutable values, so no function could affect its caller's variables at all.
Worked example
Compare with a function taking a number or a string.
def try_to_change(n):
n = n + 1
>>> x = 5
>>> try_to_change(x)
>>> x
5
def really_changes(t):
t[0] = 99
>>> u = [5]
>>> really_changes(u)
>>> u
[99]| Function | What it does | Effect on the caller |
|---|---|---|
| n = n + 1 | repoints the parameter | the caller is unaffected |
| t[0] = 99 | modifies the shared object | the caller sees it |
| the difference | assignment versus modification | and mutability |
Look at the first function.
Why: It assigns to its parameter, which repoints a local name — lesson 3b's rule that a parameter is the function's own name for the value.
Look at the second.
Why: It modifies the object the parameter refers to, and that object is the caller's list.
State the general rule.
Why: A function can never change which object a caller's name refers to. It can change the object itself, if that object is mutable.
Figure (svg): Two columns contrasting assigning to a parameter with modifying the object it refers to
The first leaves the caller alone and the second does not. The difference is whether the function assigns to the parameter or modifies what it refers to — and only the second is possible for mutable types.
Verify: Check the claim on a string parameter.
Why: A function receiving a string cannot modify it at all, because strings are immutable — so no string parameter can ever affect the caller. That is why this whole issue arrives only now, in the chapter that introduces a mutable type.
Prediction
The function uses append, which modifies.
def add_item(t):
t.append(4)
nums = [1, 2, 3]
add_item(nums)
print(nums)| Step | What happens | Result |
|---|---|---|
| the call | passes a reference | t and nums are aliases |
| t.append(4) | modifies the shared object | in place |
| print(nums) | the same object | [1, 2, 3, 4] |
Predict first
What does this print?
Correct: [1, 2, 3, 4] — append modifies the shared list, and the caller's name refers to that same list.
Why: The parameter t and the variable nums are aliases for one object, so a modification through either is visible through both. Note that t = t + [4] would have done nothing to nums, because concatenation creates a new list and the assignment would repoint only the local parameter.
Worked example
The book marks it WRONG in a comment. Work out why.
def bad_delete_head(t):
t = t[1:] # WRONG!
>>> t4 = [1, 2, 3]
>>> bad_delete_head(t4)
>>> t4
[1, 2, 3]| Part | What happens | Effect |
|---|---|---|
| t[1:] | a slice creates a NEW list | the original is untouched |
| t = ... | repoints the local parameter | the caller's arrow is unaffected |
| t4 | still refers to the original | unchanged |
Notice what the slice does.
Why: The slice operator creates a new list — it does not modify the original, exactly as lesson 10a's table said.
Notice what the assignment does.
Why: It makes t refer to the new list, which does not affect the caller.
State the situation precisely.
Why: At the beginning of bad_delete_head, t and t4 refer to the same list. At the end, t refers to a new list, but t4 still refers to the original, unmodified list.
Figure (svg): The state of the program after each line of Worked example the function that does not work, drawn as a ladder with one rung per traced line
Nothing happens to the caller's list. The function creates a new list and points its own parameter at it, which is invisible from outside.
Verify: Compare with the working version.
Why: delete_head uses del t[0], which modifies the shared object rather than creating a new one. The two functions differ by exactly the create-versus-modify distinction from lesson 10a, and here that distinction decides whether the function does anything at all.
Trap
A function is meant to change the caller's list, and it does so by assigning a new list to its parameter.
Assume that changing the parameter changes the argument
Why: The parameter refers to the caller's list, so assigning to it feels like changing that list.
Assignment repoints the parameter and leaves the caller's arrow alone. The function runs, does work, and has no effect — with no error at all.
To change the caller's list, modify the object rather than the name.
Use del, append, sort, or indexed assignment
Why: All of these change the object that both names refer to.
Or return a new list and let the caller assign it
Why: Which is often the better design, and makes the change visible at the call site.
It is important to distinguish between operations that modify lists and operations that create new lists. The append method modifies a list; the plus operator creates one. That distinction is what decides whether a function affects its caller.
Sorting
Ask whether the body modifies the object or reassigns the name.
Sort into buckets
For each function body, does the caller see a change?
Faded example
Modify the object rather than reassigning the name.
Fill in the blanks
def double_all(t):
for i in range(len(t)):
t[i] = t[i] * 2
Why: Assigning to t[i] modifies the shared list, so the caller sees the doubled values. Assigning to t itself — or to the loop variable, as in lesson 10a's trap — would repoint a local name and leave the caller's list untouched. The bracket is what makes the difference between changing an object and moving an arrow.
Explain it to yourself
This is the first chapter where a function can change a caller's data.
Discussion prompt
Explain why a function taking a number or a string can never affect its caller, using what you know about assignment and immutability.
Hint: What are the only two things a function can do to what it was given?
Answer:
A function can either reassign its parameter, which repoints its own local name, or modify the object it refers to.
Reassignment never affects the caller, because the caller's name has its own arrow. So the only route to the caller is modifying the object.
For an immutable type that route is closed — nothing can modify a number or a string — so no such function could ever affect its caller. Lists open the route for the first time, which is why the whole issue arrives in this chapter and not earlier.
Section
Section 4
Concept
Since a function may or may not modify its argument, the choice is a design decision the caller has to know about. The book gives both forms of the same operation.
# modifies the caller's list
def delete_head(t):
del t[0]
# returns a new list, leaves the caller's alone
def tail(t):
return t[1:]| Function | Its contract | Note |
|---|---|---|
| delete_head | modifies, returns None | the caller's list changes |
| tail | returns a new list | the caller's list is unchanged |
| the call site | delete_head(t) versus rest = tail(t) | the shape differs |
An alternative is to write a function that creates and returns a new list. This function leaves the original list unmodified — and the call site makes the difference visible, since one form assigns a result and the other does not.
Think Python, 2nd edition — Allen B. Downey §10.10-10.13, pp. 98-98
Picture it
The way a function is called is the clue to what it does.
Figure (svg): Two columns comparing a modifying call with a returning call at the call site
That convention is not enforced, which is why the docstring has to say which kind a function is — it is a postcondition in the sense of lesson 4b.
Worked example
Same operation, two designs, two different call sites.
>>> letters = ['a', 'b', 'c']
>>> delete_head(letters)
>>> letters
['b', 'c']
>>> letters = ['a', 'b', 'c']
>>> rest = tail(letters)
>>> rest
['b', 'c']
>>> letters
['a', 'b', 'c']| Function | What the caller ends up with | Note |
|---|---|---|
| delete_head | the caller's list is shortened | and nothing is returned |
| tail | a new list is returned | and the caller's list is intact |
| which to use | depends on whether the original is still wanted | a design decision |
Notice both produce ['b', 'c'].
Why: The two functions compute the same thing and deliver it in different ways.
Notice what happens to the original.
Why: One destroys it and one leaves it. If the caller still needs the original, only one of the two is usable.
Notice the call sites differ.
Why: One is a statement and one is an expression whose value must be assigned — which is a visible clue, though not a guarantee.
Figure (svg): The state of the program after each line of Worked example the two contracts, side by side, drawn as a ladder with one rung per traced line
Two functions with different contracts. The returning version is usually the safer default, because it cannot surprise a caller who still needed the original.
Verify: Try each with the wrong call shape.
Why: rest = delete_head(letters) gives rest = None, and tail(letters) on its own discards the result. Each has a characteristic misuse, and the two misuses are exact opposites — which is why the contract has to be documented rather than guessed.
Prediction
The function returns a new list.
def tail(t):
return t[1:]
letters = ['a', 'b', 'c']
tail(letters)
print(letters)| Line | What happens | Result |
|---|---|---|
| tail(letters) | creates and returns a new list | the original is untouched |
| no assignment | the returned list is discarded | nothing is kept |
| print(letters) | unchanged | ['a', 'b', 'c'] |
Predict first
What does this print?
Correct: ['a', 'b', 'c'] — tail returns a new list, which is discarded, and the caller's list is unmodified.
Why: This is the characteristic misuse of a returning function: calling it without assigning the result. The function did its work correctly and the result went nowhere. Compare with the modifying version, delete_head, where a call with no assignment is exactly the right usage — the two functions have opposite correct call shapes.
Worked example
When you want a modifying operation and the original, copy first.
>>> t = [3, 1, 2]
>>> t2 = t[:]
>>> t2.sort()
>>> t
[3, 1, 2]
>>> t2
[1, 2, 3]
>>> t2 = sorted(t)
>>> t
[3, 1, 2]| Approach | What happens | Note |
|---|---|---|
| t[:] | creates a genuine copy | a second object |
| t2.sort() | modifies the copy | t is unaffected |
| sorted(t) | the same result, one step | returns a new sorted list |
Copy before modifying.
Why: If you want to use a method like sort that modifies the argument, but you need to keep the original list as well, you can make a copy.
Modify the copy.
Why: The original is untouched because the copy is a different object — which the is operator would confirm.
Note the built-in alternative.
Why: You could also use sorted, which returns a new sorted list and leaves the original alone — one step instead of two.
Figure (svg): A state diagram showing t and t2 pointing at two separate lists after a slice copy
Both approaches keep the original. The copy-then-modify version generalises to any modifying method; sorted is the purpose-built version for this one case.
Verify: Check that the copy really is separate.
Why: t2 is t reports False after the slice copy, and True if you had written t2 = t instead. Confirming with is rather than by inspection is worth the habit, because two lists with the same elements look identical when printed.
Trap
A function sorts its argument in place and also returns it, so that both call shapes appear to work.
Support both usages to be helpful
Why: It makes the function convenient however it is called.
Now t2 = sort_it(t) looks like it produced a copy and did not — both names refer to one sorted list, and the caller's original is gone. The convenience hides the modification.
Pick one contract and make the call site tell the truth.
If it modifies, return None
Why: Which is what Python's own list methods do, so that the misuse fails immediately.
If it returns a new list, do not modify the argument
Why: Which is what sorted does.
Python's library follows this rule consistently — sort modifies and returns None, sorted returns and modifies nothing. Following it in your own functions is the only way a reader can tell which kind they are calling.
Comparison
Fill the blanks. Each has a characteristic misuse.
Comparison matrix
| Question | Modifying | Returning |
|---|---|---|
| What does it return? | None | a new list |
| The caller's list? | changed | unchanged |
| Correct call shape | f(t) | result = f(t) |
| Characteristic misuse | t = f(t), which stores None | f(t) alone, which discards the result |
The bottom row is the diagnostic. A NoneType error means a modifying function was assigned; an unchanged list means a returning function was called and ignored.
Discrimination
Look at what the body does and what it returns.
Sort into buckets
For each function body, which contract is it?
Explain it
The convention helps and does not guarantee.
Discussion prompt
A classmate asks how to tell whether a function will modify the list they pass it. Give them three things to check, in order of reliability.
Hint: Only one of the three is definitive.
Answer:
First and most reliable: read the docstring. Whether a function modifies its argument is a postcondition, and lesson 4b said postconditions belong in the documentation.
Second: look at what it returns. By convention a function that modifies returns None, so a function you can usefully assign probably does not modify — but the convention is not enforced.
Third, and only for library functions: the naming. Python pairs sort with sorted and reverse with reversed, where the shorter verb modifies. That is a real convention and it does not extend to code other people wrote.
Section
Section 5
Concept
The book closes the chapter with three specific suggestions, and each addresses a failure this lesson has demonstrated.
The second is the one that is hardest to follow and the most valuable. Consistency within a program removes a decision that has no interesting content, and it means a reader knows what to expect.
Think Python, 2nd edition — Allen B. Downey §10.10-10.13, pp. 98-99
Picture it
Each rule exists because of a specific bug in this chapter.
Figure (svg): Two columns pairing each debugging rule with the failure it prevents
Notice that all three failures are silent or near-silent. That is what makes lists harder than strings: the mistakes do not announce themselves.
Worked example
One short program, and each rule prevents something.
def top_three(scores):
ordered = sorted(scores) # rule 3: no copy needed
ordered.reverse() # rule 1: modifies, no assignment
result = []
for s in ordered[:3]:
result.append(s) # rule 2: one idiom for adding
return result| Line | Which rule it follows | Note |
|---|---|---|
| sorted(scores) | returns a new list | the caller's list is safe |
| ordered.reverse() | modifies, returns None | called with no assignment |
| result.append(s) | one consistent idiom | not mixed with + or += |
Use a returning function where one exists.
Why: sorted leaves the caller's list alone, so no copy is needed and the caller cannot be surprised.
Call the modifying method correctly.
Why: reverse modifies in place and returns None, so it is called on its own line with no assignment.
Use one idiom for adding.
Why: append throughout, rather than mixing it with concatenation — which is the second rule.
Figure (svg): The state of the program after each line of Worked example applying all three rules, drawn as a ladder with one rung per traced line
A function that cannot surprise its caller, whose method calls are shaped correctly, and which uses one idiom consistently. Each rule contributed one line.
Verify: Check that the caller's list is unmodified.
Why: scores is untouched, because sorted returned a new list and everything afterwards operated on that. Confirming this with scores is ordered — which reports False — is the check that the copy is genuine rather than an alias.
Error analysis
Mark each and say whether it raises.
Annotate
Three silent failures out of four is why lists need more care than strings, where nearly every mistake raises.
Worked example
Three rules broken, and only one of them raises.
def top_three(scores):
scores.sort() # modifies the CALLER's list
scores = scores.reverse() # scores becomes None
result = []
result = result.append(scores[0]) # None again
return result| Line | What goes wrong | Loud or silent |
|---|---|---|
| scores.sort() | modifies the caller's list | silent side effect |
| scores = scores.reverse() | reverse returns None | scores is destroyed |
| scores[0] | indexing None | TypeError, at last |
Find the silent bug.
Why: sort modifies the caller's list, which the caller never asked for and cannot see from the call site.
Find the destructive one.
Why: Assigning reverse's return value stores None, losing the list entirely.
Find the one that finally raises.
Why: Indexing None raises a TypeError — several lines after the actual mistake, which is what makes the diagnosis awkward.
Figure (svg): A panel showing the three mistakes with an arrow from the error back to its actual cause
Three mistakes, of which one is invisible to the caller, one destroys the data, and only the consequence of the second raises an error. The traceback points at line 5 and the cause is on line 3.
Verify: Trace the error back to its cause.
Why: The TypeError names NoneType, which is the signature from lesson 6a: a void operation's return value was assigned. Looking one line up from the failure — and then one more — finds the reverse call. That upward search is the standard diagnosis for this whole family of bug.
Trap
One function uses append, another uses concatenation, a third uses += — all in the same program.
Choose the best tool for each individual case
Why: Each choice is defensible on its own, and variety looks like sophistication.
A reader now has to think about which one is in front of them every time, and the ones that create new lists behave differently from the ones that modify — which matters as soon as a list is shared.
Pick an idiom and stick with it.
Decide once, at the start
Why: append for adding, del for removing, sorted for a sorted copy — or another set, consistently applied.
Deviate only when the alternative is genuinely required
Why: And when you do, the deviation is visible precisely because everything else is consistent.
Consistency is worth more than optimality here, because the choice has no interesting content. What it buys is that a reader stops having to check.
Prediction
Only one of the three.
t = [1, 2]
t.append([3])
t = t.append(4)
print(len(t))| Line | What happens | Note |
|---|---|---|
| t.append([3]) | adds a nested list | legal, wrong shape |
| t = t.append(4) | appends, then stores None | legal |
| len(t) | len of None | TypeError |
Predict first
Which line raises an error?
Correct: Line 4 — the TypeError comes from calling len on None, two lines after the mistake that caused it.
Why: Line 2 is legal and adds a nested list. Line 3 is legal and stores None in t, which is the actual mistake. Line 4 is the first line that cannot proceed, so that is where the traceback points. This gap between the cause and the report is exactly lesson 5c's warning that an error message says where the problem was discovered, not always where it is.
Ranking
The cause is usually above the report.
Put in order
Why: Read the error kind first, which identifies the family; then the location, which is where it was discovered; then search upward for the assignment that stored None; then fix it. This is lesson 3c's read-the-traceback procedure applied to the chapter's most common bug, and the upward search is the step that is specific to it.
Real world
The advice is about consistency rather than about lists.
Discussion prompt
Think of a context where several equally good ways of doing something exist and consistency is worth more than choosing the best one each time. What does the consistency buy?
Hint: Anything with more than one person involved.
Answer:
Date formats, filing conventions, naming schemes — all cases where any single choice would do and mixing them is worse than any of them.
What consistency buys is that nobody has to check. A reader who knows every date in a system is written one way never has to wonder whether 03/04 is March or April.
The programming version has an extra edge: some of the idioms behave differently, so mixing them is not merely inconsistent but genuinely error-prone. append modifies and concatenation creates, and a program that mixes them has two behaviours where a reader expects one.
Comparison
Fill the blanks. One implication holds and the other does not.
Comparison matrix
| Question | Equivalent | Identical |
|---|---|---|
| What does it mean? | the same value | the same object |
| Which operator tests it? | == | is |
| Does it imply the other? | no | yes — identical objects are equivalent |
| Does modifying one affect the other? | no — they are separate objects | yes — there is only one object |
The bottom row is why the distinction matters. For immutable types it never arises, which is why the whole issue waited until this chapter.
Pattern
Six steps, and the first is a question you have to ask deliberately.
Step 3's verification matters because two lists with the same elements look identical when printed. is is the only way to tell a copy from an alias by inspection.
Python documentation — Data Structures Data Structures
Check
The assignment copies a reference.
a = [1, 2, 3]
b = a
b[0] = 99| Line | What happens | Effect on a |
|---|---|---|
| b = a | one object, two names | aliased |
| b[0] = 99 | modifies the object | through b |
| a | the same object | sees the change |
Check your understanding
What does a hold after these three lines?
Answer: B
Why: b = a copies the reference rather than the list, so both names refer to one object. Modifying that object through b is visible through a, because there is only one list. Writing b = a[:] instead would have created a genuine copy and left a unchanged.
Check
The function reassigns its parameter.
def f(t):
t = t + [4]
nums = [1, 2, 3]
f(nums)| Step | What happens | Result |
|---|---|---|
| t + [4] | creates a NEW list | the original is untouched |
| t = ... | repoints the local parameter | the caller's arrow is unaffected |
| nums | unchanged | [1, 2, 3] |
Check your understanding
What does nums hold after the call?
Answer: B
Why: The plus operator creates a new list and the assignment points the parameter at it, which repoints a local name and leaves the caller's arrow alone. To affect the caller the function would have to modify the object — with append, del, or indexed assignment.
Check
One of these creates a genuine copy.
Check your understanding
Which line gives you a list you can modify without affecting the original?
Answer: B
Why: A slice with both indices omitted produces a new list containing the same elements, so backup refers to a different object. Modifying it leaves the original alone, and backup is original would report False — which is the check worth running, since the two lists print identically.
Real world
The copy-versus-reference distinction is everywhere in shared systems.
Discussion prompt
Think of a situation where you shared something and later disagreed about whether a change should have been visible — a document, a spreadsheet, a folder. What was the underlying question, and how do such systems resolve it?
Hint: Sending a link is different from sending a file.
Answer:
Sending a link is aliasing: one document, several people, and every change is visible to all of them. Sending a file is copying: several documents, and changes stay local.
Systems make the choice explicit precisely because it is the source of so much confusion — share versus download a copy is exactly the distinction between b = a and b = a[:].
And the failure mode is the same: someone believes they have a private copy, edits it, and discovers they were editing the shared original. That is the aliasing bug, and it is a genuinely hard mistake to see from either side.
Commit first
Answer, then rate your confidence. This is the chapter's central fact.
Predict first
A function's body is t = t[1:], and it is called with a list. What does the caller's list look like afterwards?
Correct: Unchanged — the slice creates a new list and the assignment repoints the local parameter, which the caller cannot see.
Why: The book marks this exact function WRONG in a comment, and the reason is the create-versus-modify distinction from lesson 10a. At the beginning of the function the parameter and the caller's variable refer to the same list; the slice creates a second list and the assignment points the parameter at it, leaving the caller's arrow exactly where it was. A function can never change which object a caller's name refers to — it can only change the object itself, which needs del, append, or indexed assignment. This is the single most important fact in the chapter, and its failure is completely silent.
Explain it
One picture explains the whole chapter.
Discussion prompt
A classmate cannot understand why changing one list changed another. Draw them the two pictures — figures 10.3 and 10.4 — and give them the one-line check that distinguishes the two situations.
Hint: The pictures differ by the number of boxes.
Answer:
Draw two boxes with one arrow each, and then one box with two arrows. Say: the first is two lists that happen to be equal; the second is one list with two names.
In the first, changing one leaves the other alone. In the second, there is only one list, so a change through either name is visible through both.
The check is a is b. True means one box and False means two — and it is the only way to tell them apart by inspection, because two equal lists print identically.
Exit ticket
One honest answer. It decides what the next lesson opens with.
Predict first
Which of these is still least solid for you?
Correct: Whichever you picked is the right answer — this one is for you, not for a mark.
Why: The object-versus-value distinction is the foundation and it is worth being pedantic about, because everything else in the lesson is stated in those terms. Aliasing is the idea that makes mutability dangerous, and drawing the two pictures until they are automatic is the best investment here. The list-arguments section is the practical payoff and the source of the chapter's most silent bug. And the debugging rules are advice with specific bugs behind them — which is what makes them worth following rather than merely agreeing with.
Connect it up
One page, from memory. The pictures are the exercise.
Draw it
Draw the book's figures 10.3 and 10.4 side by side: two names pointing at two equal lists, and two names pointing at one list. Under each, write what a is b reports and what happens when one name is used to modify. Then draw figure 10.5 — a stack diagram with a list shared between two frames — and write beside it the two things a function can do to what it was given, marking which of them the caller can see.
Recap
Four pages, and chapter 10 is finished: the idea that makes mutability both powerful and dangerous.
| If you remember one thing | It is this |
|---|---|
| From objects and values | Equivalent is about contents; identical is about which object. |
| From aliasing | b = a copies the arrow, not the box. There is still only one list. |
| From list arguments | A function can change the object, never which object your name refers to. |
| From copying | Two equal lists print identically. Only is can tell a copy from an alias. |
| From the rules | Three of the four wrong ways to append are legal. Silence is the danger. |
Chapter 11 introduces dictionaries, which map keys to values rather than positions to values — a different kind of collection, mutable like a list, and the tool that makes counting, lookup and memoization straightforward.
Think Python, 2nd edition — Allen B. Downey §10.10-10.13, pp. 95-99 — everything on these slides traces back here
Want this taught 1-on-1? Alexander tutors Python — $55/session, free consultation.