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
Title
Python · Chapter 15 — Classes and objects
§15.4-15.7, pp. 150-152
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
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.
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
Think Python, 2nd edition — Allen B. Downey §15.4-15.7, pp. 152-152
Section
Section 1
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)| Line | What it does | Note |
|---|---|---|
| p = Point() | a new instance | not the one passed in |
| p.x, p.y | computed from the rectangle | corner plus half the size |
| return p | the new object | the 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
Picture it
One object in, a different object out.
Figure (svg): A pipeline showing a Rectangle passed in and a new Point returned
That combination — reads its argument, returns something new — is the returning contract from lesson 10c, and it is the safer of the two.
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| Expression | How deep | What it gives |
|---|---|---|
| rect.width | one dot | a number on the rectangle |
| rect.corner | one dot | an embedded Point |
| rect.corner.x | two dots | a 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 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.
Prediction
A rectangle at the origin.
# box: corner (0, 0), width 100, height 200
center = find_center(box)
print_point(center)| Coordinate | Computation | Value |
|---|---|---|
| x | 0 + 100/2 | 50 |
| y | 0 + 200/2 | 100 |
| the result | a new Point | (50, 100) |
Predict first
What does this print?
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.
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| Function | Its contract | Note |
|---|---|---|
| find_center | builds and returns | nothing is modified |
| set_center | fills in a point you supply | modifies the argument |
| the choice | lesson 10c's two contracts | again |
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
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.
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.
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.
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?
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.
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.
Section
Section 2
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| Statement | What it changes | Note |
|---|---|---|
| assignment to an attribute | changes the object's state | not a name |
| inside a function | the same effect | on the caller's object |
| rect | an alias for box | one 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
Picture it
The same object, before and after.
Figure (svg): A Rectangle object shown with its width and height attributes changed in place
Which is what makes the change visible through every name that refers to it — the caller's, and the function's parameter.
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)| Stage | The values | Note |
|---|---|---|
| before | 150 and 300 | |
| the call | rect is an alias for box | one object |
| after | 200 and 400 | the 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 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.
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)| Step | What happens | Result |
|---|---|---|
| the call | rect is an alias for box | one object |
| rect.width += 50 | modifies the object | through the parameter |
| box.width | the same object | increased by 50 |
Predict first
If box.width was 150.0, what does this print?
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.
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| Part | What it does | Note |
|---|---|---|
| rect.corner | the embedded Point | step one |
| .x += dx | modifies that Point | step two |
| what changes | the Point, not the Rectangle | worth 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
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.
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.
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.
Invariant
Two operations, and they touch different objects.
Step through it
Which object does each operation modify?
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.
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.
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.
Section
Section 3
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| Expression | What it asks | Result |
|---|---|---|
| copy.copy(p1) | a duplicate object | same data |
| p1 is p2 | not the same object | False, as expected |
| p1 == p2 | also False | which 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
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
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.
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| Type | What == does | Why |
|---|---|---|
| for a list | == compares contents | Python knows what that means |
| for an instance | == compares identity | Python does not know |
| the reason | you have not said | at 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
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.
Prediction
A copy, with identical data.
p2 = copy.copy(p1)
print(p1 is p2, p1 == p2)| Operator | What it checks | Result |
|---|---|---|
| is | two different objects | False |
| == | for instances, the same as is | False |
| the data | identical | and irrelevant |
Predict first
What does this print?
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.
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| Approach | How many objects | Effect on p1 |
|---|---|---|
| p2 = p1 | copies the reference | one object |
| copy.copy(p1) | duplicates the object | two objects |
| modifying p2 | affects 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
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.
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.
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.
Comparison
Fill the blanks. One creates a name and one creates an object.
Comparison matrix
| Question | p2 = p1 | p2 = copy.copy(p1) |
|---|---|---|
| How many objects? | one | two |
| What does is report? | True | False |
| Modifying p2 affects p1? | yes | no |
| What does == report? | True — the same object | False — 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.
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.
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.
Section
Section 4
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| Expression | What it shows | Result |
|---|---|---|
| box2 is box | two Rectangles | False |
| box2.corner is box.corner | ONE Point | True |
| the consequence | size 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
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
Which is why the two operations behave differently: growing touches the duplicated attributes and moving touches the shared object.
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| Operation | What it touches | Effect on the original |
|---|---|---|
| growing | modifies box2's own attributes | independent |
| moving | modifies the shared Point | affects both |
| the inconsistency | two behaviours from one copy | confusing |
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
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.
Prediction
A shallow copy of a rectangle.
box2 = copy.copy(box)
print(box2 is box, box2.corner is box.corner)| What | Copied? | is reports |
|---|---|---|
| the Rectangle | duplicated | False |
| the embedded Point | not duplicated | True |
| the name | a shallow copy |
Predict first
What does this print?
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.
Worked example
Copy the object and everything it refers to.
>>> box3 = copy.deepcopy(box)
>>> box3 is box
False
>>> box3.corner is box.corner
False| Expression | What it shows | Result |
|---|---|---|
| box3 is box | two Rectangles | as with a shallow copy |
| box3.corner is box.corner | two Points as well | the difference |
| the result | completely 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
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.
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.
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.
Sorting
Ask which object each one modifies.
Sort into buckets
After box2 = copy.copy(box), does the operation on box2 affect box?
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.
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.
Section
Section 5
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| Function | The question | Note |
|---|---|---|
| type | which class is this? | the class object |
| isinstance | is it one of these? | True or False |
| hasattr | does 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
Picture it
Each answers something different about what you are holding.
Figure (svg): Two columns pairing each inspection function with the question it answers
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.
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 message | What it tells you | Note |
|---|---|---|
| the class | named in the message | Point |
| the attribute | also named | 'z' |
| what to check | whether it was ever assigned | or 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.
Prediction
Two attributes were assigned, and one was not.
p = Point()
p.x = 3
p.y = 4
print(hasattr(p, 'x'), hasattr(p, 'z'))| Attribute | Assigned? | Result |
|---|---|---|
| 'x' | assigned | True |
| 'z' | never assigned | False |
| the arguments | an object and a string |
Predict first
What does this print?
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.
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| Approach | What it does | Note |
|---|---|---|
| hasattr | the name as a string | 'x', in quotes |
| the try version | the same outcome | in the book's own words |
| the advantage | works with different types | more 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
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.
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.
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.
Discrimination
Four questions about an object.
Sort into buckets
For each question, which tool answers it?
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.
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.
Comparison
Fill the blanks. They differ in one place, and it is the one that matters.
Comparison matrix
| Question | copy.copy | copy.deepcopy |
|---|---|---|
| Is the top object duplicated? | yes | yes |
| Are embedded objects duplicated? | no — the references are copied | yes, and so are theirs |
| box2 is box | False | False |
| box2.corner is box.corner | True — shared | False — separate |
The bottom row is the only test that tells them apart, since the third row is False either way.
Pattern
Five steps, and the third is the one people skip.
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
Check
A copy with identical data.
p2 = copy.copy(p1)
print(p1 == p2)| Aspect | What is true | Result |
|---|---|---|
| the data | identical | and irrelevant |
| == on instances | the same as is | identity |
| the result | False |
Check your understanding
What does this print?
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__.
Check
A rectangle with an embedded point.
Check your understanding
After box2 = copy.copy(box), what does box2.corner is box.corner report?
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.
Check
The second argument has a required form.
Check your understanding
Which call correctly asks whether p has an attribute named 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.
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.
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?
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.
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.
Exit ticket
One honest answer. It decides what the next lesson opens with.
Predict first
Which of these is still least solid for you?
Correct: Whichever you picked is the right answer — this one is for you, not for a mark.
Why: 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.
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.
Recap
Three pages, and chapter 15 is finished: objects you can build, change, and copy.
| If you remember one thing | It is this |
|---|---|
| From return values | A function that returns a new object must build one, not return its argument. |
| From mutability | Be 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 copies | Resizing is independent and moving is not, from a single copy.copy. |
| From debugging | The 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
Want this taught 1-on-1? Alexander tutors Python — $55/session, free consultation.