Chapter 6: Objects — Solutions
Worked solutions to the 25 Find the Bug exercises in Chapter 6: Objects. Each one names the kind of bug, explains why the original misbehaved, and shows the corrected program.
Solutions stay folded until you open them — try the exercise first; the diagnosis is where the learning is.
Solutions 6.1.1–6.1.5
Solution 6.1.1 — Defining a class
Show the diagnosis and the fix
Bug type: Syntax
The def __init__(self, width, height) header is missing the colon at the end, so Python cannot parse the method definition. Adding the colon fixes it.
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
room = Rectangle(4, 3)
print(room.area()) # 12
Solution 6.1.2 — Using self
Show the diagnosis and the fix
Bug type: Runtime
Inside to_fahrenheit the bare name celsius is not defined; the value is stored on the instance, so it must be reached through self“.celsius. Using self.celsius fixes the NameError.
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
def to_fahrenheit(self):
return self.celsius * 9 / 5 + 32
reading = Temperature(100)
print(reading.to_fahrenheit()) # 212.0
Solution 6.1.3 — Storing an attribute
Show the diagnosis and the fix
Bug type: Runtime
The attribute stored in __init__ is named score, but summary reads self.grade, which was never set. Referring to self.score fixes the AttributeError.
class Student:
def __init__(self, name, score):
self.name = name
self.score = score
def summary(self):
return f"{self.name} scored {self.score}"
learner = Student("Ava", 88)
print(learner.summary()) # Ava scored 88
Solution 6.1.4 — Calling a method
Show the diagnosis and the fix
Bug type: Runtime
The method is called on the class, Account.add_interest(0.05), so 0.05 is bound to self and no rate is supplied, raising a TypeError. Calling it on the instance, savings.add_interest(0.05), passes savings as self and 0.05 as rate.
class Account:
def __init__(self, balance):
self.balance = balance
def add_interest(self, rate):
self.balance = self.balance + self.balance * rate
savings = Account(1000)
savings.add_interest(0.05)
print(savings.balance) # 1050.0
Solution 6.1.5 — Method calls need parentheses
Show the diagnosis and the fix
Bug type: Logical
wheel.circumference refers to the method object instead of calling it, so the printout is a function reference, not the number. Adding parentheses — wheel.circumference() — calls the method and prints 62.8318.
class Circle:
def __init__(self, radius):
self.radius = radius
def circumference(self):
return 2 * 3.14159 * self.radius
wheel = Circle(10)
print(wheel.circumference()) # 62.8318
Solutions 6.2.1–6.2.5
Solution 6.2.1 — Instance vs class attribute
Show the diagnosis and the fix
Bug type: Runtime
The class attribute is named gravity_unit, but the last line reads earth.unit, which does not exist. Using earth.gravity_unit fixes the AttributeError.
class Planet:
gravity_unit = "m/s^2"
def __init__(self, name, mass):
self.name = name
self.mass = mass
earth = Planet("Earth", 5.97e24)
mars = Planet("Mars", 6.42e23)
print(earth.name, earth.gravity_unit) # Earth m/s^2
print(mars.name, mars.gravity_unit) # Mars m/s^2
print(earth.gravity_unit) # m/s^2
Solution 6.2.2 — Shared mutable class attribute
Show the diagnosis and the fix
Bug type: Logical
laps is a class attribute, so a single list is shared by every runner; amir.record and beth.record append to the same list. Each runner needs its own list, created as an instance attribute in __init__.
class Runner:
def __init__(self, name):
self.name = name
self.laps = []
def record(self, seconds):
self.laps.append(seconds)
amir = Runner("Amir")
beth = Runner("Beth")
amir.record(58)
beth.record(61)
print(amir.laps) # [58]
print(beth.laps) # [61]
Solution 6.2.3 — Updating a class counter
Show the diagnosis and the fix
Bug type: Logical
Writing self.count = self.count + 1 reads the class value but creates a new instance attribute, leaving Book.count at 0. Incrementing Book.count updates the shared class counter, so it reaches 3.
class Book:
count = 0
def __init__(self, title):
self.title = title
Book.count = Book.count + 1
Book("Algebra")
Book("Biology")
Book("Chemistry")
print(Book.count) # 3
Solution 6.2.4 — Instance overriding shared default
Show the diagnosis and the fix
Bug type: Logical
Thermostat.target_c = 22 reassigns the shared class attribute, so both rooms read 22 and the bedroom does not keep the default. Assigning to the instance, kitchen.target_c = 22, creates an instance attribute that shadows the class default for the kitchen only, leaving the bedroom at 20.
class Thermostat:
target_c = 20
def __init__(self, room):
self.room = room
kitchen = Thermostat("kitchen")
bedroom = Thermostat("bedroom")
kitchen.target_c = 22 # set only this instance
print(kitchen.target_c) # 22
print(bedroom.target_c) # 20
Solution 6.2.5 — Reading a class constant
Show the diagnosis and the fix
Bug type: Logical
Sales tax is added by multiplying the subtotal by the rate, not dividing by it. The corrected line uses self.subtotal + self.subtotal * self.tax_rate, giving 54.0.
class Cart:
tax_rate = 0.08
def __init__(self, subtotal):
self.subtotal = subtotal
def total(self):
return self.subtotal + self.subtotal * self.tax_rate
groceries = Cart(50.0)
print(groceries.total()) # 54.0
Solutions 6.3.1–6.3.5
Solution 6.3.1 — Relying on a shared method
Show the diagnosis and the fix
Bug type: Runtime
shape.area refers to the method without calling it, so adding it to a number raises a TypeError. Calling shape.area() invokes each shape’s method and totals the areas to 25.0.
class Square:
def __init__(self, side):
self.side = side
def area(self):
return self.side * self.side
class Triangle:
def __init__(self, base, height):
self.base = base
self.height = height
def area(self):
return self.base * self.height / 2
def total_area(shapes):
total = 0
for shape in shapes:
total = total + shape.area()
return total
print(total_area([Square(4), Triangle(6, 3)])) # 25.0
Solution 6.3.2 — Same behavior, different types
Show the diagnosis and the fix
Bug type: Runtime
greet calls animal.speka(), but both classes provide speak, not speka, so every call raises an AttributeError. Correcting the method name to animal.speak() lets duck typing work for any object that can speak.
class Cat:
def speak(self):
return "Meow"
class Cow:
def speak(self):
return "Moo"
def greet(animal):
print("The animal says:", animal.speak())
greet(Cat())
greet(Cow()) # The animal says: Moo
Solution 6.3.3 — Behavior over type
Show the diagnosis and the fix
Bug type: Runtime
Both objects expose a length attribute, but meters_of reads thing.size, which neither class has. Reading thing.length fixes the AttributeError, and any object with a length works.
class Field:
def __init__(self, length):
self.length = length
class Pool:
def __init__(self, length):
self.length = length
def meters_of(thing):
return thing.length
print(meters_of(Field(100))) # 100
print(meters_of(Pool(25))) # 25
Solution 6.3.4 — Duck typing a method call
Show the diagnosis and the fix
Bug type: Logical
device.tick only looks up the method; it never calls it, so watch.seconds stays 0. Adding parentheses — device.tick() — actually advances each device.
class Stopwatch:
def __init__(self):
self.seconds = 0
def tick(self):
self.seconds = self.seconds + 1
class Metronome:
def __init__(self):
self.beats = 0
def tick(self):
self.beats = self.beats + 1
def advance(devices):
for device in devices:
device.tick()
watch = Stopwatch()
advance([watch])
print(watch.seconds) # 1
Solution 6.3.5 — Trusting the interface
Show the diagnosis and the fix
Bug type: Logical
sensor.read returns the bound method instead of the reading, so the printout is a method reference, not a number. Calling sensor.read() returns the value from whichever sensor was passed.
class TempSensor:
def read(self):
return 21.5
class HumiditySensor:
def read(self):
return 48.0
def sample(sensor):
return sensor.read()
print(sample(TempSensor())) # 21.5
print(sample(HumiditySensor())) # 48.0
Solutions 6.4.1–6.4.5
Solution 6.4.1 — A list is mutable
Show the diagnosis and the fix
Bug type: Runtime
The list has three items at indexes 0, 1, and 2, so tokyo_temps[3] is out of range and raises an IndexError. The typo is at index 0, so the fix assigns to tokyo_temps[0].
tokyo_temps = [33.0, 36.5, 31.0]
tokyo_temps[0] = 33.5
print(tokyo_temps) # [33.5, 36.5, 31.0]
Solution 6.4.2 — Aliasing a list
Show the diagnosis and the fix
Bug type: Logical
list(readings) builds a brand-new list, so same_readings is a separate object and appending to it does not change readings. To make both names refer to the same list, assign directly with same_readings = readings.
readings = [12, 15, 9]
same_readings = readings
same_readings.append(20)
print(readings) # [12, 15, 9, 20]
Solution 6.4.3 — Appending to a list
Show the diagnosis and the fix
Bug type: Logical
n + n doubles each number instead of squaring it, so the list comes out [2, 4, 6, 8, 10]. Squaring with n * n (or n ** 2) produces the intended [1, 4, 9, 16, 25].
squares = []
for n in range(1, 6):
squares.append(n * n)
print(squares) # [1, 4, 9, 16, 25]
Solution 6.4.4 — Indexing the last item
Show the diagnosis and the fix
Bug type: Runtime
len(scores) is 4, but valid indexes are 0 through 3, so scores[len(scores)] raises an IndexError. The last item is at scores[len(scores) - 1], or more simply scores[-1].
scores = [91, 87, 95, 78]
latest = scores[-1]
print(latest) # 78
Solution 6.4.5 — A list method
Show the diagnosis and the fix
Bug type: Runtime
list.remove deletes by value, not by index, so tasks.remove(1) looks for the value 1 and raises a ValueError. To drop the finished task by value, remove "report" (or use del tasks[1]).
tasks = ["email", "report", "lunch"]
tasks.remove("report")
print(tasks) # ['email', 'lunch']
Solutions 6.5.1–6.5.5
Solution 6.5.1 — Assignment is not a copy
Show the diagnosis and the fix
Bug type: Logical
backup = temps makes both names point at the same list, so appending to temps also changes backup. Taking a real copy with temps.copy() (or temps[:]) keeps the backup unchanged.
temps = [33.0, 36.5, 31.0]
backup = temps.copy()
temps.append(34.0)
print(backup) # [33.0, 36.5, 31.0]
Solution 6.5.2 — Making a shallow copy
Show the diagnosis and the fix
Bug type: Runtime
prices.copy refers to the method without calling it, so copy_of_prices becomes a method object and .append raises an AttributeError. Calling prices.copy() makes the independent copy.
prices = [1.99, 2.49, 0.99]
copy_of_prices = prices.copy()
copy_of_prices.append(5.00)
print(prices) # [1.99, 2.49, 0.99]
print(copy_of_prices) # [1.99, 2.49, 0.99, 5.0]
Solution 6.5.3 — Copying with a slice
Show the diagnosis and the fix
Bug type: Logical
The slice grades[0:2] copies only the first two items, so working starts as [85, 90] and the result is wrong. A full-list slice grades[:] copies every element.
grades = [85, 90, 78]
working = grades[:]
working.append(100)
print(grades) # [85, 90, 78]
print(working) # [85, 90, 78, 100]
Solution 6.5.4 — Deep copy for nested lists
Show the diagnosis and the fix
Bug type: Logical
copy.copy makes a shallow copy, so the inner lists are still shared and appending to independent[0] also changes grid. copy.deepcopy copies the nested lists recursively, keeping the original intact.
import copy
grid = [[1, 2], [3, 4]]
independent = copy.deepcopy(grid)
independent[0].append(99)
print(grid) # [[1, 2], [3, 4]]
print(independent) # [[1, 2, 99], [3, 4]]
Solution 6.5.5 — Shallow copy shares inner lists
Show the diagnosis and the fix
Bug type: Logical
seats.copy() is a shallow copy: the outer list is new, but the row lists are shared, so new_seats[0].append also changes seats. Using copy.deepcopy copies each row, leaving the original chart unchanged.
import copy
seats = [["A1", "A2"], ["B1", "B2"]]
new_seats = copy.deepcopy(seats)
new_seats[0].append("A3")
print(seats) # [['A1', 'A2'], ['B1', 'B2']]
print(new_seats) # [['A1', 'A2', 'A3'], ['B1', 'B2']]