10c Objects, Values, Aliasing, and List Arguments

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

What this lesson covers

The lesson, slide by slide

1. Lesson 10c Objects, Values, Aliasing, and List Arguments

Title

Python · Chapter 10 — Lists

§10.10-10.13, pp. 95-99

2. By the end of this lesson you can

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

3. Before we start: are these the same list?

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.

4. The one idea behind this lesson: an object has a value

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

The book's figures 10.3 and 10.4. Same elements in both cases; not the same situation.

Think Python, 2nd edition — Allen B. Downey §10.10-10.13, pp. 95-96

5. Objects, values, and the is operator

Section

Section 1

6. Two questions that look like one

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
CaseWhat happenedNote
two stringsPython created only one string objectboth names refer to it
two liststwo separate list objectswith equal values
isasks about identity, not equalitythe 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

7. Picture it: figures 10.2 and 10.3

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

The book's figure 10.3: two objects, equal in value, separate in identity.

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.

8. Worked example: is and == asking different questions

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
ExpressionThe question it asksAnswer
a == bdo they have the same value?True: same elements
a is bare they the same object?False: two objects
the relationshipidentical implies equivalent, not the reverseone-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

The whole run at once: each drop is one line of the program.

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.

9. Predict: what does is report?

Prediction

Two lists with the same elements.

>>> a = [1, 2, 3]
>>> b = [1, 2, 3]
>>> a is b
StepThe questionAnswer
two list literalstwo separate objects createdeach with its own box
a is bare they the same object?no
a == bdo they have the same value?yes

Predict first

What does a is b report?

  • True
  • False
  • An error
  • It depends on the elements

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.

10. Worked example: why the string case is different

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
AspectWhat is trueNote
the stringsPython may share one objectan optimisation
why it is safestrings are immutableno one can change it
why it does not mattersharing is undetectableexcept 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

Immutability is what makes sharing safe. That is the whole reason the two types differ here.

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.

11. Trap: using is where == was meant

Trap

The 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.

The fix

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.

12. Discriminate: is or ==?

Discrimination

Ask whether you care about identity or about contents.

Sort into buckets

For each question, which operator answers it?

the is operator
are these two names referring to one list?; did this function return None?; would changing one of these affect the other?
the equality operator
do these two lists contain the same elements?; is this number equal to 42?; are these two strings the same text?
is
Each is genuinely about identity: whether two names denote one object, or whether a value is the unique None object. The third one is the standard idiom — x is None rather than x == None.
eq
Each is about value. Contents, magnitude, and text are all questions about what an object holds rather than about which object it is.

13. Two truths and a lie: objects and values

Two truths and a lie

Two are true. Keep the lie.

Eliminate the wrong options

Rule out the two true statements.

  • A. If two objects are identical, they are also equivalent
  • B. Two lists can be equivalent without being identical
  • C. If two objects are equivalent, they are identical

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.

14. Think it through: why does Python share strings and not lists?

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.

15. Aliasing

Section

Section 2

16. One object, more than one name

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]
LineWhat happensNote
b = acopies the reference, not the listone object, two names
b[0] = 42modifies the objectthrough b
asees the changebecause 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

17. Picture it: figure 10.4, two arrows to one box

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

The book's figure 10.4. One box, two arrows — which is what aliasing means.

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.

18. Worked example: a change through one alias

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]
LineWhat happensEffect on a
b = ab refers to the same objectno copy is made
b[0] = 42the OBJECT is modifiednot the name
arefers to that objectso 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

The whole run at once: each drop is one line of the program.

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.

19. Predict: what does a hold?

Prediction

One assignment, and then a modification through the other name.

a = [1, 2, 3]
b = a
b.append(4)
print(a)
LineWhat happensResult
b = aone object, two namesaliased
b.append(4)modifies the shared objectin place
print(a)the same object[1, 2, 3, 4]

Predict first

What does this print?

  • [1, 2, 3]
  • [1, 2, 3, 4]
  • [4]
  • None

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.

20. Worked example: assignment versus modification, one more time

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]
LineWhat it doesEffect on a
b = [9, 9, 9]repoints b at a NEW objecta is unaffected
b[0] = 9modifies the SHARED objecta sees it
the differenceone moves an arrow, one changes a boxand 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

One bracket is the difference between these two.

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.

21. Trap: expecting b = a to make a copy

Trap

The 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.

The fix

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.

22. Watch the references: aliasing and reassignment

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?

  1. One list object exists, with one arrow pointing at it from a.
  2. b = a adds a second arrow to the SAME object. No new list is created.
  3. The object is modified through b. Both arrows still point at it, so a now shows [42, 2, 3].
  4. b is repointed at a new list. The first object is unchanged and a still refers to it, so a is still [42, 2, 3].

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.

23. Complete it: make a real copy

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.

24. Explain it: why did my original change?

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.

25. List arguments: a function can change your list

Section

Section 3

26. The parameter is an alias

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']
PartWhat happensNote
the callpasses a referencenot a copy
t and lettersaliases for the same objecttwo frames, one list
del t[0]modifies the shared objectletters 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

27. Picture it: figure 10.5, the list between the frames

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

Two frames, two names, and one list object that both refer to.

This is genuinely new. Every function before this chapter received immutable values, so no function could affect its caller's variables at all.

28. Worked example: why this is new

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]
FunctionWhat it doesEffect on the caller
n = n + 1repoints the parameterthe caller is unaffected
t[0] = 99modifies the shared objectthe caller sees it
the differenceassignment versus modificationand 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

One changes the function's own name; the other changes something the caller can see.

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.

29. Predict: does the caller see the change?

Prediction

The function uses append, which modifies.

def add_item(t):
    t.append(4)

nums = [1, 2, 3]
add_item(nums)
print(nums)
StepWhat happensResult
the callpasses a referencet and nums are aliases
t.append(4)modifies the shared objectin place
print(nums)the same object[1, 2, 3, 4]

Predict first

What does this print?

  • [1, 2, 3]
  • [1, 2, 3, 4]
  • [4]
  • None

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.

30. Worked example: the function that does not work

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]
PartWhat happensEffect
t[1:]a slice creates a NEW listthe original is untouched
t = ...repoints the local parameterthe caller's arrow is unaffected
t4still refers to the originalunchanged

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

The whole run at once: each drop is one line of the program.

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.

31. Trap: writing a modifying function that assigns to its parameter

Trap

The 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.

The fix

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.

32. Sort: does this function affect its caller's list?

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?

the caller sees the change
t.append(4); del t[0]; t[0] = 99
the caller is unaffected
t = t + [4]; t = t[1:]; t = sorted(t)
yes
Each modifies the object that the parameter refers to, and the caller's name refers to the same object. append, del and indexed assignment all change the list in place.
no
Each creates a new list and assigns it to the parameter, which repoints a local name. The caller's arrow still points at the original, unmodified list.

33. Complete it: make the function affect its caller

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.

34. Explain it yourself: why could no earlier function do this?

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.

35. Modify or return: designing the function

Section

Section 4

36. Two contracts, and the caller cannot tell which

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:]
FunctionIts contractNote
delete_headmodifies, returns Nonethe caller's list changes
tailreturns a new listthe caller's list is unchanged
the call sitedelete_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

37. Picture it: two shapes at the call site

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

A call with no assignment usually modifies; one whose result is assigned usually does not.

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.

38. Worked example: the two contracts, side by side

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']
FunctionWhat the caller ends up withNote
delete_headthe caller's list is shortenedand nothing is returned
taila new list is returnedand the caller's list is intact
which to usedepends on whether the original is still wanteda 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

The whole run at once: each drop is one line of the program.

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.

39. Predict: what does the caller end up with?

Prediction

The function returns a new list.

def tail(t):
    return t[1:]

letters = ['a', 'b', 'c']
tail(letters)
print(letters)
LineWhat happensResult
tail(letters)creates and returns a new listthe original is untouched
no assignmentthe returned list is discardednothing is kept
print(letters)unchanged['a', 'b', 'c']

Predict first

What does this print?

  • ['b', 'c']
  • ['a', 'b', 'c']
  • None
  • An error

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.

40. Worked example: making a copy to get both

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]
ApproachWhat happensNote
t[:]creates a genuine copya second object
t2.sort()modifies the copyt is unaffected
sorted(t)the same result, one stepreturns 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

Two boxes, so sorting one leaves the other alone.

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.

41. Trap: a function that modifies AND returns

Trap

The 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.

The fix

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.

42. Compare: the two contracts

Comparison

Fill the blanks. Each has a characteristic misuse.

Comparison matrix

QuestionModifyingReturning
What does it return?Nonea new list
The caller's list?changedunchanged
Correct call shapef(t)result = f(t)
Characteristic misuset = f(t), which stores Nonef(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.

43. Discriminate: which contract does this function have?

Discrimination

Look at what the body does and what it returns.

Sort into buckets

For each function body, which contract is it?

modifies, returns None
del t[0]; t.sort(); t.append(x)
returns a new list, modifies nothing
return t[1:]; return sorted(t); return t + [x]
mod
Each changes the object the parameter refers to and has no return statement, so the call produces None and the caller's list is different afterwards.
ret
Each builds a new list and returns it, leaving the argument alone. The caller must assign the result or it is lost.

44. Explain it: how do I know which kind a function is?

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.

45. The debugging rules for lists

Section

Section 5

46. Three pieces of advice, each earned

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

47. Picture it: three rules, three failures

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

Advice with a specific bug behind it is worth more than advice without one.

Notice that all three failures are silent or near-silent. That is what makes lists harder than strings: the mistakes do not announce themselves.

48. Worked example: applying all three rules

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
LineWhich rule it followsNote
sorted(scores)returns a new listthe caller's list is safe
ordered.reverse()modifies, returns Nonecalled with no assignment
result.append(s)one consistent idiomnot 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

The whole run at once: each drop is one line of the program.

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.

49. Error analysis: four lines, three silent failures

Error analysis

Mark each and say whether it raises.

Annotate

  • Line 1 adds a nested list as a single element, so the list grows by one and contains something of the wrong shape. Legal, and silently wrong.
  • Line 2 assigns append's return value, which is None, destroying the reference to the list. Legal at this point, and the failure appears on the next operation.
  • Line 3 computes a new list and discards it, because the result is not assigned. Legal, and it has no effect at all.
  • Line 4 tries to concatenate a list and a non-list, which raises a TypeError. This is the only one of the four that fails immediately.
  • So three of the four are legal and do the wrong thing, and only one announces itself — which is exactly the book's point about testing them in interactive mode.
  • The correct forms are t.append(x), t = t + [x], and t += [x], and picking one of them consistently avoids the whole family.

Three silent failures out of four is why lists need more care than strings, where nearly every mistake raises.

50. Worked example: the same program, done badly

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
LineWhat goes wrongLoud or silent
scores.sort()modifies the caller's listsilent side effect
scores = scores.reverse()reverse returns Nonescores is destroyed
scores[0]indexing NoneTypeError, 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.

51. Trap: mixing idioms within one program

Trap

The 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.

The fix

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.

52. Predict: which line raises?

Prediction

Only one of the three.

t = [1, 2]
t.append([3])
t = t.append(4)
print(len(t))
LineWhat happensNote
t.append([3])adds a nested listlegal, wrong shape
t = t.append(4)appends, then stores Nonelegal
len(t)len of NoneTypeError

Predict first

Which line raises an error?

  • Line 2
  • Line 3
  • Line 4
  • None of them

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.

53. Rank: the diagnosis of a NoneType error

Ranking

The cause is usually above the report.

Put in order

  1. read the error: TypeError mentioning NoneType
  2. note the line the traceback points at
  3. look one line up for a method call on the right of an equals sign
  4. remove the assignment, calling the method on its own line

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.

54. Where *pick an idiom* applies beyond code

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.

55. Compare: equivalent and identical

Comparison

Fill the blanks. One implication holds and the other does not.

Comparison matrix

QuestionEquivalentIdentical
What does it mean?the same valuethe same object
Which operator tests it?==is
Does it imply the other?noyes — identical objects are equivalent
Does modifying one affect the other?no — they are separate objectsyes — 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.

56. The procedure: working safely with a shared list

Pattern

Six steps, and the first is a question you have to ask deliberately.

  1. Ask whether more than one name refers to this list — including a parameter, which is always a second name.
  2. If it is shared and you want to change it, make sure every holder expects the change.
  3. If it is shared and you do not want them to see it, copy first with t[:], and verify with is that the copy is genuine.
  4. When writing a function, decide whether it modifies or returns, and document that in the docstring.
  5. If it modifies, return None so that the wrong call shape fails; if it returns, do not modify the argument.
  6. Call modifying methods on their own line, and always assign the result of a function that returns one.

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

57. Check yourself 1 of 3: aliasing

Check

The assignment copies a reference.

a = [1, 2, 3]
b = a
b[0] = 99
LineWhat happensEffect on a
b = aone object, two namesaliased
b[0] = 99modifies the objectthrough b
athe same objectsees the change

Check your understanding

What does a hold after these three lines?

  • A. [1, 2, 3]
  • B. [99, 2, 3] (correct)
  • C. None
  • D. An error is raised

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.

Why A tempts people
This would be the answer if the assignment had made a copy. It copies the arrow, not the box.
Why C tempts people
Nothing here returns None. Indexed assignment is a statement that modifies, and neither name is reassigned.
Why D tempts people
Nothing illegal happens. Modifying a list through any of its names is exactly what mutability allows.

58. Check yourself 2 of 3: list arguments

Check

The function reassigns its parameter.

def f(t):
    t = t + [4]

nums = [1, 2, 3]
f(nums)
StepWhat happensResult
t + [4]creates a NEW listthe original is untouched
t = ...repoints the local parameterthe caller's arrow is unaffected
numsunchanged[1, 2, 3]

Check your understanding

What does nums hold after the call?

  • A. [1, 2, 3, 4]
  • B. [1, 2, 3] (correct)
  • C. None
  • D. [4]

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.

Why A tempts people
This would require the function to modify the shared list. t.append(4) would do it; t = t + [4] does not.
Why C tempts people
The function returns None, but nums was never assigned the result, so it is unaffected either way.
Why D tempts people
Nothing replaces the caller's list. A function cannot change which object a caller's name refers to.

59. Check yourself 3 of 3: copying

Check

One of these creates a genuine copy.

Check your understanding

Which line gives you a list you can modify without affecting the original?

  • A. backup = original
  • B. backup = original[:] (correct)
  • C. backup is original
  • D. backup == 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.

Why A tempts people
This copies the reference, not the list. Both names would refer to one object and modifying either would affect both.
Why C tempts people
is is a comparison, not an assignment. It produces True or False and creates nothing.
Why D tempts people
== is also a comparison. It reports whether the values match, which tells you nothing about whether a copy was made.

60. Where this shows up outside this course

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.

61. Confidence wager: commit before you check

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?

  • Missing its first element
  • Unchanged
  • Empty
  • It becomes None

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.

62. Explain it to someone else

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.

63. Exit ticket

Exit ticket

One honest answer. It decides what the next lesson opens with.

Predict first

Which of these is still least solid for you?

  • Objects versus values, and what the is operator asks
  • Aliasing, and why it matters only for mutable objects
  • How a function can — and cannot — change its caller's list
  • The debugging rules: return values, one idiom, and making copies

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.

64. Synthesis: draw the map of this lesson

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.

65. What you can do now

Recap

Four pages, and chapter 10 is finished: the idea that makes mutability both powerful and dangerous.

If you remember one thingIt is this
From objects and valuesEquivalent is about contents; identical is about which object.
From aliasingb = a copies the arrow, not the box. There is still only one list.
From list argumentsA function can change the object, never which object your name refers to.
From copyingTwo equal lists print identically. Only is can tell a copy from an alias.
From the rulesThree 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

Sources

  1. Think Python, 2nd edition — Allen B. Downey — Allen B. Downey, Think Python: How to Think Like a Computer Scientist, 2nd edition (Green Tea Press, 2015), §10.10-10.13, pp. 95-99
  2. Python documentation — Data Structures
  3. Python documentation — Expressions

Want this taught 1-on-1? Alexander tutors Python — $55/session, free consultation.

Book on Wyzant · Text (657) 465-8108