15b Instances as Return Values, Mutability, and Copying

This lesson returns objects from functions, modifies objects through their attributes, copies instances with the copy module, explains why a shallow copy of a nested object is error-prone, and covers the tools for debugging attribute problems.

Subject: Python · 65 slides · code lesson

Open the interactive version of this deck

What this lesson covers

The lesson, slide by slide

1. Lesson 15b Instances as Return Values, Mutability, and Copying

Title

Python · Chapter 15 — Classes and objects

§15.4-15.7, pp. 150-152

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 §15.4-15.7, pp. 150-152 — the pages these objectives are drawn from

3. Before we start: copying a rectangle

Warm-up

The rectangle holds a Point.

Discussion prompt

You copy a Rectangle so you can move the copy without disturbing the original. Given that its corner attribute refers to a separate Point object, what could go wrong?

Hint: What exactly gets copied?

Answer:

The copy might duplicate the Rectangle and not the Point it refers to — so both rectangles would share one corner.

Then resizing the copy would leave the original alone, because width and height are numbers held directly, and moving it would change both, because the corner is shared.

Two attributes behaving one way and one behaving another is exactly what makes this bug hard to see. The book calls it confusing and error-prone, and this lesson is largely about it.

4. The one idea behind this lesson: copying has a depth

Concept

The copy module contains a function called copy that can duplicate any object. It copies the object and any references it contains, but not the embedded objects — which is called a shallow copy.

shallow copy — To copy the contents of an object, including any references to embedded objects.

For most applications this is not what you want. The copy module also provides deepcopy, which copies not only the object but also the objects it refers to, and the objects they refer to, and so on.

Figure (svg): Two columns contrasting a shallow copy with a deep copy of a nested object

Two of the attributes behave the same way in both columns. Only the embedded object differs.

Think Python, 2nd edition — Allen B. Downey §15.4-15.7, pp. 152-152

5. Instances as return values

Section

Section 1

6. A function that builds an object

Concept

Functions can return instances. find_center takes a Rectangle as an argument and returns a Point that contains the coordinates of the centre of the Rectangle.

def find_center(rect):
    p = Point()
    p.x = rect.corner.x + rect.width/2
    p.y = rect.corner.y + rect.height/2
    return p

>>> center = find_center(box)
>>> print_point(center)
(50, 100)
LineWhat it doesNote
p = Point()a new instancenot the one passed in
p.x, p.ycomputed from the rectanglecorner plus half the size
return pthe new objectthe caller assigns it

Nothing about returning an instance is special — it is an ordinary return value. What matters is that the function built a new object rather than modifying the one it was given.

Think Python, 2nd edition — Allen B. Downey §15.4-15.7, pp. 150-150

7. Picture it: a function that makes something

Picture it

One object in, a different object out.

Figure (svg): A pipeline showing a Rectangle passed in and a new Point returned

The rectangle is read and not modified; the point is new.

That combination — reads its argument, returns something new — is the returning contract from lesson 10c, and it is the safer of the two.

8. Worked example: reading through two dots

Worked example

The centre needs the corner's coordinates.

p.x = rect.corner.x + rect.width/2

# rect.corner    -> the embedded Point
# rect.corner.x  -> its x coordinate
# rect.width     -> a number, held directly
ExpressionHow deepWhat it gives
rect.widthone dota number on the rectangle
rect.cornerone dotan embedded Point
rect.corner.xtwo dotsa number on that Point

Read the one-dot attributes.

Why: width and height are numbers held directly on the rectangle.

Read the embedded object.

Why: rect.corner gives the Point itself, which is an object rather than a number.

Go one step further.

Why: rect.corner.x reaches into that Point for its coordinate.

Figure (svg): The state of the program after each line of Worked example reading through two dots, drawn as a ladder with one rung per traced line

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

The centre's x is the corner's x plus half the width. Two of the three values needed one dot and one needed two, which is the shape of every nested structure.

Verify: Check the answer against the numbers.

Why: A rectangle at (0, 0) with width 100 and height 200 has its centre at (50, 100), which is what the book's example prints. Checking with a corner at the origin makes the arithmetic easy to verify, and it is worth repeating with a corner elsewhere to confirm the corner term is really being added.

9. Predict: what does find_center return?

Prediction

A rectangle at the origin.

# box: corner (0, 0), width 100, height 200
center = find_center(box)
print_point(center)
CoordinateComputationValue
x0 + 100/250
y0 + 200/2100
the resulta new Point(50, 100)

Predict first

What does this print?

  • (50, 100)
  • (100, 200)
  • (0, 0)
  • The Point object's address

Correct: (50, 100) — the corner plus half the width and half the height.

Why: print_point formats the coordinates, which is why the address in option D does not appear — printing the Point directly would give that instead. Option B is the size rather than the centre, and option C is the corner, which would be the answer if the halved dimensions were not added.

10. Worked example: why it returns a new Point

Worked example

The alternative would be to modify something.

# returns a new object: safe
def find_center(rect):
    p = Point()
    ...
    return p

# would modify the caller's point: a different contract
def set_center(rect, p):
    p.x = rect.corner.x + rect.width/2
    p.y = rect.corner.y + rect.height/2
FunctionIts contractNote
find_centerbuilds and returnsnothing is modified
set_centerfills in a point you supplymodifies the argument
the choicelesson 10c's two contractsagain

Note what find_center does not touch.

Why: It reads the rectangle and creates its own Point, so neither the rectangle nor anything the caller holds is modified.

Note the alternative.

Why: A function could take a Point to fill in, which avoids creating one and requires the caller to supply it.

Prefer the returning form here.

Why: A centre is a new value rather than a change to something existing, so building it is the natural fit.

Figure (svg): Two columns comparing a function that returns a new Point with one that fills in a supplied Point

Lesson 10c's two contracts, chosen here by what the result is.

The returning contract, chosen because the result is a new value. Nothing forces it — the modifying version would work — and the call sites would look quite different.

Verify: Check that find_center leaves the rectangle alone.

Why: Printing box.width and box.corner.x afterwards shows them unchanged, which confirms the function only reads. That is worth checking in general for any function taking a mutable object, since nothing in the signature promises it.

11. Trap: returning the argument instead of a new object

Trap

The trap

A function meant to produce a shifted point modifies its argument and returns it.

Return the result of the work

Why: The object does hold the answer once the work is done.

The caller now has a second name for the object they passed in, so their original is gone — modified in place — and the returned value gives no clue that this happened.

The fix

Build a new instance when the contract says returning.

p = Point() inside the function

Why: Which is what find_center does on its first line.

And assign every attribute

Why: A partly built object fails later, at the missing attribute.

A function that both modifies and returns is the trap from lesson 10c: it looks like a copy was made, and no copy was made. Pick one contract, and let the call site tell the truth.

12. Discriminate: how many dots does this need?

Discrimination

Some values are held directly and some are one level in.

Sort into buckets

For each value of a Rectangle, how deep is it?

one dot
the width; the height; the corner Point itself; half the height
two dots
the corner's x coordinate; the corner's y coordinate
one
Each is an attribute held directly on the rectangle — including the corner itself, which is one dot away even though it is an object, and a computation on a one-dot value.
two
Each is inside the embedded Point, so the first dot reaches the Point and the second reaches its coordinate.

13. Complete it: build the centre point

Faded example

The function needs its own object.

Fill in the blanks

def find_center(rect):
p = Point()
p.x = rect.corner.x + rect.width/2
p.y = rect.corner.y + rect.height/2
return p

Why: Calling the class creates a new instance for the function to fill in and return, so nothing the caller holds is touched. Writing p = rect.corner instead would alias the rectangle's own corner, and the two assignments would then move the rectangle rather than computing its centre.

14. Explain it: why does the function make its own Point?

Explain it

It could have used the one it was given.

Discussion prompt

A classmate asks why find_center creates a Point rather than reusing rect.corner, which is already a Point. Explain.

Hint: What would the assignments do?

Answer:

Because assigning to that Point's attributes would modify the rectangle. p = rect.corner makes p an alias, and p.x = ... then moves the rectangle's corner.

The function would return the right coordinates and destroy the rectangle in the process — and the caller would have no indication, since the return value looks correct.

So creating a new object is what makes the function a reading function rather than a modifying one. That distinction is invisible in the signature, which is why the first line matters as much as the arithmetic.

15. Objects are mutable

Section

Section 2

16. Assign to an attribute and the object changes

Concept

You can change the state of an object by making an assignment to one of its attributes. To change the size of a rectangle without changing its position, you can modify the values of width and height.

box.width = box.width + 50
box.height = box.height + 100

def grow_rectangle(rect, dwidth, dheight):
    rect.width += dwidth
    rect.height += dheight
StatementWhat it changesNote
assignment to an attributechanges the object's statenot a name
inside a functionthe same effecton the caller's object
rectan alias for boxone object

Inside the function, rect is an alias for box, so when the function modifies rect, box changes. That is the third time the book has made this point — for lists, for Points, and now for Rectangles — because it is the same rule each time.

Think Python, 2nd edition — Allen B. Downey §15.4-15.7, pp. 151-151

17. Picture it: the state changes, the object does not move

Picture it

The same object, before and after.

Figure (svg): A Rectangle object shown with its width and height attributes changed in place

Two attributes changed and the object is the same one it was.

Which is what makes the change visible through every name that refers to it — the caller's, and the function's parameter.

18. Worked example: grow_rectangle in action

Worked example

The function returns nothing and changes the caller's object.

>>> box.width, box.height
(150.0, 300.0)
>>> grow_rectangle(box, 50, 100)
>>> box.width, box.height
(200.0, 400.0)
StageThe valuesNote
before150 and 300
the callrect is an alias for boxone object
after200 and 400the caller sees it

Note the call shape.

Why: No assignment: grow_rectangle(box, 50, 100) is a statement, which is the modifying contract's call shape.

Note the aliasing.

Why: Inside the function, rect is an alias for box, so when the function modifies rect, box changes.

Note that += works on attributes.

Why: rect.width += dwidth reads the attribute, adds, and stores it back — the same expansion as anywhere else.

Figure (svg): The state of the program after each line of Worked example grow rectangle in action, drawn as a ladder with one rung per traced line

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

The caller's rectangle grows. Nothing is returned, and the change is the whole effect of the call.

Verify: Check that the corner did not move.

Why: box.corner.x and box.corner.y are unchanged, which is what changing the size without changing the position means for this representation. With the two-corners representation from the previous lesson, growing the rectangle would have to move a corner — which is the design decision showing its consequences.

19. Predict: what does the caller see?

Prediction

The function modifies its argument.

def grow_rectangle(rect, dw, dh):
    rect.width += dw
    rect.height += dh

grow_rectangle(box, 50, 100)
print(box.width)
StepWhat happensResult
the callrect is an alias for boxone object
rect.width += 50modifies the objectthrough the parameter
box.widththe same objectincreased by 50

Predict first

If box.width was 150.0, what does this print?

  • 200.0
  • 150.0
  • 50.0
  • None

Correct: 200.0 — inside the function, rect is an alias for box, so modifying rect modifies box.

Why: Assigning to an attribute changes the object's state, and the change is visible through every reference to it. Writing rect = Rectangle() inside the function would repoint the local parameter and leave the caller's rectangle exactly as it was — the modify-versus-reassign distinction, unchanged since chapter 10.

20. Worked example: moving the rectangle

Worked example

The exercise, and it reaches through the embedded object.

def move_rectangle(rect, dx, dy):
    rect.corner.x += dx
    rect.corner.y += dy
PartWhat it doesNote
rect.cornerthe embedded Pointstep one
.x += dxmodifies that Pointstep two
what changesthe Point, not the Rectangleworth noticing

Reach the corner.

Why: rect.corner gives the embedded Point, which is where the position lives.

Modify its coordinates.

Why: It should change the location of the rectangle by adding dx to the x coordinate of corner and dy to the y coordinate.

Notice which object is modified.

Why: The Point, not the Rectangle — the rectangle's own attributes are untouched, and its corner still refers to the same Point.

Figure (svg): A state diagram showing a rectangle whose corner Point is modified in place

Moving the rectangle modifies the Point, not the Rectangle.

The rectangle moves by modifying the object its corner attribute refers to. That third step is the one that makes the shallow-copy problem in idea 4 possible.

Verify: Check that the rectangle's own attributes did not change.

Why: width, height and corner are all exactly what they were — corner still refers to the same Point, which now holds different numbers. Being precise about which object changed is what makes the copying section make sense, because a shallow copy shares that Point.

21. Trap: reassigning the corner instead of moving it

Trap

The trap

A move function writes rect.corner = Point() and sets the new point's coordinates.

Give the rectangle a new corner

Why: Which does put it in the right place.

Anything else referring to the old Point still refers to the old Point — so a second rectangle sharing that corner is left behind, and code holding a reference to it sees no movement.

The fix

Modify the existing Point.

rect.corner.x += dx

Why: Which changes the object rather than replacing it.

Unless replacing is what you mean

Why: In which case say so, since it breaks any sharing.

The two are genuinely different operations with different consequences for anything sharing the corner — which is exactly the situation a shallow copy creates, and why the next ideas matter.

22. Watch the state: growing and moving

Invariant

Two operations, and they touch different objects.

Step through it

Which object does each operation modify?

  1. The rectangle holds two numbers directly and a reference to a Point.
  2. Growing changes width and height — attributes of the Rectangle itself. The Point is untouched.
  3. Moving changes the Point's coordinates. The Rectangle's own three attributes are untouched, including corner, which still refers to the same Point.
  4. After both, the Rectangle has new numbers and its Point has new numbers, and no object was replaced.

Growing modifies the Rectangle and moving modifies the Point. That difference is invisible while there is one rectangle, and it is exactly what makes a shallow copy behave inconsistently.

23. Complete it: move the rectangle

Faded example

The position lives in the embedded Point.

Fill in the blanks

def move_rectangle(rect, dx, dy):
rect.corner.x += dx
rect.corner.y += dy

Why: The rectangle's position is the corner Point's coordinates, so moving it means reaching through corner and modifying x. Assigning to rect.x instead would create a new and meaningless attribute on the Rectangle, which would be legal and would move nothing.

24. Explain it yourself: which object changed?

Explain it to yourself

Two operations that both change the rectangle.

Discussion prompt

Growing and moving both change the rectangle, in ordinary English. Explain precisely which object each one modifies, and why it will matter.

Hint: Where does each value live?

Answer:

Growing modifies the Rectangle: width and height are numbers held directly on it, so the assignment changes the Rectangle's own state.

Moving modifies the Point: the position lives in the embedded object, and the Rectangle's three attributes — including corner — are untouched.

It matters as soon as two rectangles share a corner, which a shallow copy arranges. Then growing one leaves the other alone and moving one moves both, which is the inconsistency the book calls confusing and error-prone.

25. Copying, and why == disappoints

Section

Section 3

26. copy.copy duplicates an object

Concept

Aliasing can make a program difficult to read, because changes in one place might have unexpected effects in another, and it is hard to keep track of all the variables that might refer to a given object. Copying an object is often an alternative.

>>> import copy
>>> p2 = copy.copy(p1)
>>> print_point(p1)
(3, 4)
>>> print_point(p2)
(3, 4)
>>> p1 is p2
False
>>> p1 == p2
False
ExpressionWhat it asksResult
copy.copy(p1)a duplicate objectsame data
p1 is p2not the same objectFalse, as expected
p1 == p2also Falsewhich is the surprise

p1 and p2 contain the same data, but they are not the same Point. The is operator reports False, which is what we expected — and you might have expected == to yield True, because these points contain the same data.

Think Python, 2nd edition — Allen B. Downey §15.4-15.7, pp. 151-152

27. Picture it: two objects with the same data

Picture it

A copy is a second object, and == does not notice.

Figure (svg): A state diagram showing two separate Point objects holding identical coordinates

Two boxes with the same contents — the picture of a copy, and == still reports False.

For a list, the same picture would give == True. The difference is that Python knows what makes two lists equal and does not know what makes two Points equal.

28. Worked example: why == compares identity

Worked example

The book's explanation, and the phrase at the end of it.

>>> p1 == p2
False        # even though the data match

# for instances, the default behaviour of ==
# is the same as is: it checks object IDENTITY,
# not object equivalence
TypeWhat == doesWhy
for a list== compares contentsPython knows what that means
for an instance== compares identityPython does not know
the reasonyou have not saidat least, not yet

State the behaviour.

Why: For instances, the default behaviour of the == operator is the same as the is operator: it checks object identity, not object equivalence.

State the reason.

Why: For programmer-defined types, Python doesn't know what should be considered equivalent.

Note the promise.

Why: The book adds at least, not yet — which points at the __eq__ method two chapters ahead.

Figure (svg): Two columns contrasting equality for a list with equality for a programmer-defined instance

The same picture, and two different answers — because one type came with an opinion and the other did not.

False, by design rather than by accident. Python has no way to know whether two Points with the same coordinates should count as equal, so it falls back on the one question it can always answer.

Verify: Ask why a list is different.

Why: Because a list's contents are the whole of what it is, so comparing them is unambiguous. A Point could reasonably be equal by coordinates, or never equal to anything but itself, or equal within a tolerance — and only its author knows which. Falling back on identity is the choice that assumes nothing.

29. Predict: what does == report?

Prediction

A copy, with identical data.

p2 = copy.copy(p1)
print(p1 is p2, p1 == p2)
OperatorWhat it checksResult
istwo different objectsFalse
==for instances, the same as isFalse
the dataidenticaland irrelevant

Predict first

What does this print?

  • False False
  • False True
  • True True
  • True False

Correct: False False — for instances, the default behaviour of == is the same as is.

Why: The second False is the surprise. Python doesn't know what should be considered equivalent for a programmer-defined type, so it falls back on identity — the one question it can always answer. For a list the same picture would give False True, because a list's contents are the whole of what it is.

30. Worked example: copying instead of aliasing

Worked example

The reason to copy at all.

# aliasing: one object, two names
p2 = p1
p2.x = 99.0
# p1.x is now 99.0 too

# copying: two objects
p2 = copy.copy(p1)
p2.x = 99.0
# p1.x is unchanged
ApproachHow many objectsEffect on p1
p2 = p1copies the referenceone object
copy.copy(p1)duplicates the objecttwo objects
modifying p2affects p1 only in the first case

Recall the problem.

Why: Aliasing can make a program difficult to read, because changes in one place might have unexpected effects in another.

Note the difficulty precisely.

Why: It is hard to keep track of all the variables that might refer to a given object.

Use a copy when independence is wanted.

Why: copy.copy can duplicate any object, so modifications to one cannot reach the other.

Figure (svg): The state of the program after each line of Worked example copying instead of aliasing, drawn as a ladder with one rung per traced line

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

Two objects rather than two names, so changes stay local. This is the slice trick from chapter 10, generalised to any object.

Verify: Confirm the copy is genuine with is.

Why: p1 is p2 reports False after copy.copy and True after plain assignment — which is the only reliable way to tell them apart, since both print identically and, for instances, both compare equal to nothing. Checking with is is the habit worth keeping.

31. Trap: comparing instances with ==

Trap

The trap

A program tests whether two Points are the same location with p1 == p2.

Compare the values, as with numbers or strings

Why: Which is what == means everywhere else.

For instances, == checks identity, so two Points at exactly the same location report False. The test silently never matches, and the program concludes the points are always different.

The fix

Compare the attributes, until you can define equality.

p1.x == p2.x and p1.y == p2.y

Why: Which says exactly what you mean.

Or define __eq__ on the class, later

Why: Which is what the book's at least, not yet is pointing at.

The silence is the danger: the comparison is legal and always False, so nothing draws attention to it. If a program never finds two objects equal, this is the first thing to suspect.

32. Compare: aliasing and copying

Comparison

Fill the blanks. One creates a name and one creates an object.

Comparison matrix

Questionp2 = p1p2 = copy.copy(p1)
How many objects?onetwo
What does is report?TrueFalse
Modifying p2 affects p1?yesno
What does == report?True — the same objectFalse — instances compare by identity

The bottom row is the awkward one: == is True for an alias and False for a copy, which is the reverse of what most people expect from an equality test.

33. Complete it: duplicate an object

Faded example

The copy module has the function.

Fill in the blanks

import copy
p2 = copy.copy(p1)
print(p1 is p2) # False

Why: copy.copy duplicates any object, so p2 refers to a different object with the same data. Writing p2 = p1 instead would copy the reference and leave one object with two names — and is would then report True, which is how you tell the two situations apart.

34. Think it through: why does Python refuse to guess?

Socratic

It could have compared the attributes.

Discussion prompt

Python could compare two instances by comparing all their attributes. Why does it compare identity instead?

Hint: When would attribute-by-attribute comparison be wrong?

Answer:

Because it is not always right. Two objects might be equal ignoring some attribute — a cached value, a timestamp, an identifier — and comparing everything would report them different.

And some types should never be equal to anything but themselves: two bank accounts with identical balances are not the same account, and treating them as equal would be a serious error.

So Python doesn't know what should be considered equivalent, and rather than guess it answers the question it can answer. Defining __eq__ is how you tell it — which is what the book's at least, not yet is promising.

35. Shallow and deep copies

Section

Section 4

36. The copy stops at the first level

Concept

If you use copy.copy to duplicate a Rectangle, you will find that it copies the Rectangle object but not the embedded Point.

deep copy — To copy the contents of an object as well as any embedded objects, and any objects embedded in them, and so on.

>>> box2 = copy.copy(box)
>>> box2 is box
False
>>> box2.corner is box.corner
True
ExpressionWhat it showsResult
box2 is boxtwo RectanglesFalse
box2.corner is box.cornerONE PointTrue
the consequencesize is independent, position is shared

This is called a shallow copy because it copies the object and any references it contains, but not the embedded objects. For most applications, this is not what you want.

Think Python, 2nd edition — Allen B. Downey §15.4-15.7, pp. 152-152

37. Picture it: figure 15.3, two rectangles and one point

Picture it

The shallow copy duplicated the box and shared its corner.

Figure (svg): A state diagram showing two Rectangle objects whose corner attributes refer to one shared Point

The book's figure 15.3. Two rectangles, and one point between them.

Which is why the two operations behave differently: growing touches the duplicated attributes and moving touches the shared object.

38. Worked example: the inconsistency this creates

Worked example

The book's own sentence about it is the clearest statement.

box2 = copy.copy(box)

grow_rectangle(box2, 50, 100)
# box is unaffected: width and height were copied

move_rectangle(box2, 5, 5)
# box MOVES TOO: the corner Point is shared
OperationWhat it touchesEffect on the original
growingmodifies box2's own attributesindependent
movingmodifies the shared Pointaffects both
the inconsistencytwo behaviours from one copyconfusing

Grow the copy.

Why: Invoking grow_rectangle on one of the Rectangles would not affect the other, because width and height were duplicated.

Move the copy.

Why: But invoking move_rectangle on either would affect both, because the position lives in a Point they share.

Name the problem.

Why: The book's verdict: this behaviour is confusing and error-prone.

Figure (svg): Two columns showing how growing and moving a shallow copy affect the original differently

One copy, two behaviours — which the book calls confusing and error-prone.

Two operations on the same copy, with opposite consequences for the original. Nothing in the code suggests they should differ.

Verify: Trace which object each operation modifies.

Why: Growing assigns to box2.width — an attribute of box2 itself, which the copy duplicated. Moving assigns to box2.corner.x — an attribute of the Point, which the copy shared. The two behave differently because they reach different depths, and that is exactly what a shallow copy makes visible.

39. Predict: do they share the corner?

Prediction

A shallow copy of a rectangle.

box2 = copy.copy(box)
print(box2 is box, box2.corner is box.corner)
WhatCopied?is reports
the RectangleduplicatedFalse
the embedded Pointnot duplicatedTrue
the namea shallow copy

Predict first

What does this print?

  • False True
  • False False
  • True True
  • True False

Correct: False True — the Rectangle was copied and the embedded Point was not.

Why: A shallow copy copies the object and any references it contains, but not the embedded objects — so box2 is a new Rectangle whose corner attribute holds the same reference the original held. copy.deepcopy would give False False, which is the answer that makes the copies genuinely independent.

40. Worked example: deepcopy

Worked example

Copy the object and everything it refers to.

>>> box3 = copy.deepcopy(box)
>>> box3 is box
False
>>> box3.corner is box.corner
False
ExpressionWhat it showsResult
box3 is boxtwo Rectanglesas with a shallow copy
box3.corner is box.cornertwo Points as wellthe difference
the resultcompletely separate objects

Use deepcopy.

Why: It copies not only the object but also the objects it refers to, and the objects they refer to, and so on.

Check both levels.

Why: Both is comparisons now report False, which is what makes the copies independent.

Conclude.

Why: box3 and box are completely separate objects, so any operation on one leaves the other alone.

Figure (svg): The state of the program after each line of Worked example deepcopy, drawn as a ladder with one rung per traced line

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

Two rectangles and two points. Both operations are now independent, which is the consistent behaviour you almost always want.

Verify: Check the depth explicitly.

Why: box3.corner is box.corner reporting False is the whole test — it is the one comparison that distinguishes a deep copy from a shallow one, since both make box3 is box False. Checking the top level alone would not tell you which kind of copy you have.

41. Trap: testing a copy only at the top level

Trap

The trap

A program checks box2 is box, sees False, and concludes the copy is independent.

Verify the copy with is

Why: Which is the right instinct, and is exactly what distinguishes a copy from an alias.

A shallow copy passes that test and still shares every embedded object. The check confirms only that the outermost object was duplicated, which is the part that was never in doubt.

The fix

Check the embedded objects too.

box2.corner is box.corner should be False

Why: Which is the test that actually distinguishes the two kinds of copy.

Or use deepcopy when the object contains others

Why: And then the check is a formality.

The rule of thumb: if the object has an attribute that is itself an object, copy.copy is probably not what you want. The book says as much — for most applications, this is not what you want.

42. Sort: which operations are independent after a shallow copy?

Sorting

Ask which object each one modifies.

Sort into buckets

After box2 = copy.copy(box), does the operation on box2 affect box?

box is unaffected
box2.width = 500; box2.height += 10; grow_rectangle(box2, 1, 1)
box changes too
box2.corner.x = 5; move_rectangle(box2, 1, 1); box2.corner.y += 3
no
Each modifies an attribute of box2 itself — width or height — and those were duplicated by the copy, so the two rectangles have their own.
yes
Each reaches through corner into the shared Point, which the shallow copy did not duplicate. Both rectangles refer to it, so both see the change.

43. Complete it: make a genuinely independent copy

Faded example

The rectangle contains another object.

Fill in the blanks

box3 = copy.deepcopy(box)
print(box3.corner is box.corner) # False

Why: deepcopy copies not only the object but also the objects it refers to, and the objects they refer to, and so on — so the corner is duplicated too. copy.copy would leave that check reporting True, which is the shallow copy the book calls confusing and error-prone.

44. Explain it: why does resizing work and moving not?

Explain it

One copy, two behaviours.

Discussion prompt

A classmate made a shallow copy of a rectangle. Resizing the copy leaves the original alone; moving it moves both. Explain why, and give them the fix.

Hint: Where does each value live?

Answer:

Width and height are numbers held directly on the Rectangle, and the copy duplicated those — so each rectangle has its own.

The position lives in a Point, and the copy duplicated the reference to it rather than the Point itself. Both rectangles refer to one Point, so moving either moves both.

The fix is copy.deepcopy, which copies the object and everything it refers to. And the test that would have caught it is box2.corner is box.corner — checking only box2 is box confirms the part that was never in doubt.

45. Debugging objects

Section

Section 5

46. Four ways to ask what you are holding

Concept

When you start working with objects, you are likely to encounter some new exceptions. If you try to access an attribute that doesn't exist, you get an AttributeError.

>>> p.z
AttributeError: Point instance has no attribute 'z'
>>> type(p)
<class '__main__.Point'>
>>> isinstance(p, Point)
True
>>> hasattr(p, 'x')
True
>>> hasattr(p, 'z')
False
FunctionThe questionNote
typewhich class is this?the class object
isinstanceis it one of these?True or False
hasattrdoes it have this attribute?the name as a STRING

The first argument of hasattr can be any object; the second argument is a string that contains the name of the attribute. You can also use a try statement to see if the object has the attributes you need.

Think Python, 2nd edition — Allen B. Downey §15.4-15.7, pp. 152-153

47. Picture it: three questions about an object

Picture it

Each answers something different about what you are holding.

Figure (svg): Two columns pairing each inspection function with the question it answers

Three ways to ask before acting, and one way to act and handle the failure.

That last pair is chapter 14's check-or-try choice again, and the same reasoning applies: the try approach can make it easier to write functions that work with different types.

48. Worked example: diagnosing an AttributeError

Worked example

The message names the class and the attribute.

>>> p = Point()
>>> p.x = 3
>>> p.y = 4
>>> p.z
AttributeError: Point instance has no attribute 'z'
Part of the messageWhat it tells youNote
the classnamed in the messagePoint
the attributealso named'z'
what to checkwhether it was ever assignedor misspelled

Read the class name.

Why: It confirms what kind of object you actually had, which is often the surprise — the error may be that the object is not what you expected.

Read the attribute name.

Why: Then ask whether it was ever assigned, and whether the spelling matches the assignment.

Look at what the object has.

Why: vars(p) lists the attributes that exist, which usually makes a typo obvious immediately.

Figure (svg): A panel showing an AttributeError with its two informative halves marked

An error message that names both halves of the problem. Most AttributeErrors are either a typo at the assignment or an object of the wrong type.

Verify: Distinguish the two causes.

Why: If the class in the message is not the class you expected, the object is wrong; if it is right, the attribute is wrong. Reading the class name first splits the investigation in two, and it is the half people skip.

49. Predict: what does hasattr report?

Prediction

Two attributes were assigned, and one was not.

p = Point()
p.x = 3
p.y = 4
print(hasattr(p, 'x'), hasattr(p, 'z'))
AttributeAssigned?Result
'x'assignedTrue
'z'never assignedFalse
the argumentsan object and a string

Predict first

What does this print?

  • True False
  • True True
  • False False
  • A NameError about z

Correct: True False — hasattr reports whether the object has an attribute of that name.

Why: The second argument is a string containing the name, so hasattr can ask about an attribute that does not exist without raising — which is exactly what p.z would do. Option D is what hasattr(p, z) without quotes would give, since a bare z would be looked up as a variable.

50. Worked example: hasattr, and the try alternative

Worked example

Ask first, or attempt and handle.

# ask first
if hasattr(p, 'x'):
    x = p.x
else:
    x = 0

# or attempt and handle
try:
    x = p.x
except AttributeError:
    x = 0
ApproachWhat it doesNote
hasattrthe name as a string'x', in quotes
the try versionthe same outcomein the book's own words
the advantageworks with different typesmore in §17.9

Note hasattr's argument.

Why: The first argument can be any object; the second is a string containing the name of the attribute — so it is quoted, which catches people.

Note the try alternative.

Why: You can also use a try statement to see if the object has the attributes you need.

Note why it is preferred.

Why: This approach can make it easier to write functions that work with different types.

Figure (svg): The state of the program after each line of Worked example hasattr, and the try alternative, drawn as a ladder with one rung per traced line

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

Two ways to cope with an attribute that may be missing. The book prefers the try version, and points forward to where the technique becomes important.

Verify: Check what hasattr does about the quotes.

Why: hasattr(p, x) without quotes looks up a variable named x and passes its value, which is almost never an attribute name — so it reports False for the wrong reason, or raises NameError. The string argument is the part to get right.

51. Trap: passing hasattr an unquoted name

Trap

The trap

A program writes hasattr(p, x), by analogy with p.x.

Write the attribute the way you always write it

Why: p.x uses a bare name, so hasattr looks like it should too.

The second argument is a string containing the name, so an unquoted x is a variable. If one exists, its value is used as the attribute name and the answer is meaningless; if not, it raises NameError.

The fix

Quote the attribute name.

hasattr(p, 'x')

Why: The name, as a string.

Or use the try approach and avoid the question

Why: Which the book suggests, and which works with different types.

The quoting is the difference between naming an attribute and using one — the same distinction as a dictionary key that is a string rather than a variable, which lesson 11a's trap was about.

52. Discriminate: which tool answers this?

Discrimination

Four questions about an object.

Sort into buckets

For each question, which tool answers it?

type
exactly which class is this object?; what class does the error message name?
isinstance
is this object a Point?; is this a Rectangle or something else?
hasattr or vars
does this object have an attribute called width?; which attributes does this object actually have?
ty
Both ask which class an object belongs to, and type returns the class object itself — which is also what an AttributeError message names.
isi
Both are yes-or-no questions about membership of a particular class, which is what isinstance answers.
has
Both are about the attributes rather than the class — hasattr for one name, and vars for the whole set.

53. Complete it: check for an attribute

Faded example

The name is a string.

Fill in the blanks

if hasattr(p, 'x'):
x = p.x

Why: The first argument can be any object and the second is a string containing the name of the attribute, so the quotes are required. Passing an unquoted x would look up a variable of that name and use its value, which is almost never an attribute name — and raises NameError if no such variable exists.

54. Where *ask or attempt* comes up again

Real world

The same choice as chapter 14's file handling.

Discussion prompt

hasattr asks first and try attempts and handles. Where else have you seen this choice, and what settled it there?

Hint: Opening a file.

Answer:

Chapter 14, with os.path.exists against a try statement — and the argument there was that the failures cannot be enumerated and a check can go stale.

Here the enumeration problem is smaller: an attribute either exists or does not, and hasattr answers exactly that. So the case for try is weaker and still real.

The book's reason is different: the try approach can make it easier to write functions that work with different types, because it asks whether the operation works rather than whether the object is a particular kind of thing. That idea gets its own section at 17.9, and it is worth carrying forward.

55. Compare: shallow and deep copies

Comparison

Fill the blanks. They differ in one place, and it is the one that matters.

Comparison matrix

Questioncopy.copycopy.deepcopy
Is the top object duplicated?yesyes
Are embedded objects duplicated?no — the references are copiedyes, and so are theirs
box2 is boxFalseFalse
box2.corner is box.cornerTrue — sharedFalse — separate

The bottom row is the only test that tells them apart, since the third row is False either way.

56. The procedure: copying an object safely

Pattern

Five steps, and the third is the one people skip.

  1. Ask whether any attribute is itself an object, rather than a number or a string.
  2. If none is, copy.copy is enough — there is nothing deeper to share.
  3. If any is, use copy.deepcopy, which copies the objects it refers to and theirs in turn.
  4. Verify with is at the embedded level, not just at the top: obj2.part is obj.part should be False.
  5. Remember that == will still report False for two instances, however identical their data.

Step 4 is what catches a shallow copy. The top-level check passes for both kinds, so it confirms only the part that was never in doubt.

Python documentation — Classes Classes

57. Check yourself 1 of 3: comparing instances

Check

A copy with identical data.

p2 = copy.copy(p1)
print(p1 == p2)
AspectWhat is trueResult
the dataidenticaland irrelevant
== on instancesthe same as isidentity
the resultFalse

Check your understanding

What does this print?

  • A. False (correct)
  • B. True
  • C. A TypeError, since instances cannot be compared
  • D. It depends on the attribute values

Answer: A

Why: For instances, the default behaviour of == is the same as the is operator: it checks object identity, not object equivalence. That is because for programmer-defined types, Python doesn't know what should be considered equivalent — at least, not until you define __eq__.

Why B tempts people
This is what a list would give, since a list's contents are the whole of what it is. An instance is not compared that way by default.
Why C tempts people
The comparison is perfectly legal, which is what makes it dangerous — it is always False and never complains.
Why D tempts people
The values are not consulted at all. Only identity is.

58. Check yourself 2 of 3: shallow copies

Check

A rectangle with an embedded point.

Check your understanding

After box2 = copy.copy(box), what does box2.corner is box.corner report?

  • A. True — the embedded Point was not duplicated (correct)
  • B. False — everything was duplicated
  • C. True — because copies always share everything
  • D. An AttributeError, since copies have no corner

Answer: A

Why: A shallow copy copies the object and any references it contains, but not the embedded objects — so both rectangles refer to one Point. Growing either is then independent and moving either affects both, which the book calls confusing and error-prone.

Why B tempts people
That is deepcopy, which copies the objects it refers to and theirs in turn.
Why C tempts people
The Rectangle itself was duplicated: box2 is box reports False. Only the embedded object is shared.
Why D tempts people
The copy has every attribute the original had, including corner — which is precisely the problem.

59. Check yourself 3 of 3: hasattr

Check

The second argument has a required form.

Check your understanding

Which call correctly asks whether p has an attribute named x?

  • A. hasattr(p, 'x') (correct)
  • B. hasattr(p, x)
  • C. hasattr(p.x)
  • D. p.hasattr('x')

Answer: A

Why: The first argument can be any object; the second argument is a string that contains the name of the attribute. The quotes are what distinguish naming an attribute from using one.

Why B tempts people
An unquoted x is a variable, so this passes its value as the attribute name — meaningless, or a NameError if no such variable exists.
Why C tempts people
This evaluates p.x first, which raises AttributeError if the attribute is missing — the very thing the check was meant to avoid.
Why D tempts people
hasattr is a built-in function, not a method on the object.

60. Where this shows up outside this course

Real world

Duplicating something that contains references is a general problem.

Discussion prompt

Think of copying something that pointed at something else — a document with links, a template referring to shared assets, a folder of shortcuts. What did the copy actually duplicate?

Hint: Did the links point at the copies or the originals?

Answer:

Usually the container and not the things it referred to: a duplicated document whose links still point at the originals, or a copied folder whose shortcuts lead back to the old files.

Which produces exactly the shallow-copy inconsistency — editing the copy's own content is independent, and following a link reaches the shared thing.

So the question to ask of any copy is how deep it went, and the test is to change something at each level and see what else moved. That is the same check as box2.corner is box.corner, in a setting without an is operator.

61. Confidence wager: commit before you check

Commit first

Answer, then rate your confidence.

Predict first

You make box2 = copy.copy(box) of a Rectangle with an embedded corner Point. Which operations on box2 also change box?

  • Moving it, but not resizing it
  • Both moving and resizing
  • Neither — the copy is independent
  • Resizing it, but not moving it

Correct: Moving it, but not resizing it — the Rectangle's own attributes were duplicated and the embedded Point was shared.

Why: This is the chapter's most valuable fact and the book's own example of why a shallow copy is confusing and error-prone. copy.copy copies the object and any references it contains, but not the embedded objects — so box2 has its own width and height, and its corner attribute holds the same reference box's does. Resizing assigns to box2.width, which is the copy's own attribute, so box is unaffected; moving assigns to box2.corner.x, which reaches through into the shared Point, so box moves too. Two operations on one copy with opposite consequences, and nothing in the code suggests they should differ. copy.deepcopy fixes it by copying the objects it refers to and theirs in turn, and the test that distinguishes the two is box2.corner is box.corner — since box2 is box reports False either way.

62. Explain it to someone else

Explain it

A copy that is only half a copy.

Discussion prompt

A classmate copied an object and is surprised that changing one attribute of the copy affected the original while changing another did not. Explain the pattern and give them the two-line diagnosis.

Hint: Which attributes are objects?

Answer:

The attributes that behaved independently are numbers or strings held directly, which copy.copy duplicated. The one that did not is an object, and only the reference to it was copied.

So the two objects share everything one level down, and any change reached through that attribute is visible to both — which is why the behaviour looks inconsistent rather than simply wrong.

The diagnosis is one comparison per embedded attribute: copy.part is original.part. If it reports True, that part is shared. And the fix is copy.deepcopy, which copies the object and everything it refers to.

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?

  • Returning instances from functions
  • Modifying an object through its attributes, and what the caller sees
  • Copying, and why == reports False for two identical instances
  • Shallow against deep copies, and diagnosing an AttributeError

Correct: Whichever you picked is the right answer — this one is for you, not for a mark.

Why: Returning an instance is an ordinary return value, and the thing worth noticing is that the function builds its own object rather than modifying its argument. The mutability is chapter 10 unchanged, though it is worth being precise about which object each operation modifies — because that precision is what makes the copying section make sense. The == surprise catches everyone once and never fails silently in an obvious way, which makes it worth remembering. And the shallow copy is the chapter's real content: two attributes behaving one way and one behaving another, from a single call.

64. Synthesis: draw the map of this lesson

Connect it up

One page, from memory.

Draw it

Draw the book's figure 15.3 — two Rectangles whose corner attributes point at one shared Point — and write beside it what grow_rectangle and move_rectangle each do to the other rectangle. Underneath, write the two comparisons that distinguish a shallow copy from a deep one, and note which of them is the same for both. Finally list the four ways to interrogate an object: the error message, type, isinstance and hasattr, with the question each answers.

65. What you can do now

Recap

Three pages, and chapter 15 is finished: objects you can build, change, and copy.

If you remember one thingIt is this
From return valuesA function that returns a new object must build one, not return its argument.
From mutabilityBe precise about which object an operation modifies; it decides what copying does.
From ==For instances it means is. Python does not know what equivalent should mean.
From shallow copiesResizing is independent and moving is not, from a single copy.copy.
From debuggingThe AttributeError names the class first — check that before the attribute.

The next chapter goes back to designing classes properly: a Time class built up through several versions, prototype-and-patch against planned development, and the moment the functions become methods.

Think Python, 2nd edition — Allen B. Downey §15.4-15.7, pp. 150-152 — 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), §15.4-15.7, pp. 150-152
  2. Python documentation — Classes
  3. Python documentation — Brief Tour of the Standard Library — Part II

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

Book on Wyzant · Text (657) 465-8108