MGU Computer Science Series — Programming with Python
Part VI: Object-Oriented and Larger Programs
State · Attributes · Methods · Objects · Design
Chapter 27 of 30
27
MGU Computer Science Series  ·  Programming with Python  ·  Library Textbook Edition

Chapter 27:
Methods, Attributes,
and Object State

How objects remember, how methods read and change what they hold, and how to design object behavior that is clear, safe, and reliable

■ Part VI of VII ■

■ 10 Sections  •  Chapter 27 of 30 ■

Builds on Chapter 26 — Classes and Objects — assumes basic familiarity with class definitions, __init__, and method syntax

Chapter Introduction and Learning Objectives

Objects are not just containers for data. They are participants in a program — things that know their own condition and can take actions based on it.

Chapter 26 introduced the core ideas of object-oriented programming: classes as blueprints, objects as individual instances, the __init__ method for setup, attributes for stored values, and methods for behavior. You wrote your first classes and created your first objects.

This chapter goes deeper. The focus is on object state: the collection of attribute values that an object carries at any moment, and how methods read and change that state over time. You will learn how to write methods that inspect what an object knows, methods that update what an object holds, and how to design objects that behave consistently and clearly.

By the end of this chapter, you should be able to read and trace programs where multiple objects exist and interact, build objects that maintain state across many method calls, work with lists of objects, give objects useful string representations, and decide when a class-based design is genuinely clearer than a dictionary-based one.

This chapter is still introductory. It does not cover inheritance, class methods, static methods, or operator overloading. Those belong to a more advanced course. The goal here is to use classes comfortably for the kinds of programs that appear naturally in a first Python course.

10Sections
10Objectives
10Key Terms
30Total Chapters

Learning Objectives

By the end of this chapter, you should be able to:

  1. 1Define object state and explain how it changes over time as methods are called.
  2. 2Read a class definition and identify all instance attributes created in __init__.
  3. 3Trace a sequence of method calls and record the state of an object's attributes after each one.
  4. 4Write methods that read state without modifying it.
  5. 5Write methods that modify state, including validation logic that rejects invalid inputs.
  6. 6Write programs where objects pass themselves or receive other objects as arguments.
  7. 7Work with lists of objects: create them, iterate over them, filter them, and accumulate from them.
  8. 8Write a __str__ method and explain what it does and why it is useful.
  9. 9Explain the tradeoffs between using a dictionary and using a class to represent a record with related behavior.
  10. 10Apply principles of clear class design: one responsibility, all attributes in __init__, methods for state changes.

Sections at a Glance

Sections 27.1 – 27.2
What Objects Hold: State and Instance Attributes
Object State · Instance Attributes · Separate Copies · Reading and Setting

Object State

When you hold a library card, that card has a state. It might currently be active or expired. It might have two books checked out. It might have a fine of zero dollars or a fine of four dollars. The card's state changes over time as you borrow books, return them, and pay fines.

An object works the same way. Its state is the collection of values stored in its attributes at a given moment. State changes whenever a method modifies an attribute.

Here is a simple LibraryCard class:

Python — LibraryCard class
class LibraryCard:

    def __init__(self, owner_name):
        self.owner = owner_name
        self.books_checked_out = 0
        self.fine_amount = 0.0
        self.active = True

When you create a card:

card = LibraryCard("Priya Nair")

The object immediately has a state:

AttributeValue
owner"Priya Nair"
books_checked_out0
fine_amount0.0
activeTrue

This state is private to that object. If you create a second card:

card2 = LibraryCard("Omar Castillo")

That object has its own completely separate state. Changing card has no effect on card2.

State is what makes objects useful. A dictionary could hold the same four values. But a class lets you attach methods — behaviors — directly to the data, so the object knows how to manage itself.

Key Idea

Tracing the state of an object across a sequence of method calls is one of the most important skills in object-oriented programming. Before you can debug or extend a class, you must be able to predict what each method call does to the object's attributes. Tracing tables, introduced in the exercises of this chapter, make this concrete.

Instance Attributes

Instance attributes are the variables attached to a specific object. They are created using self.attribute_name inside the class. Each object gets its own copy.

You saw in Chapter 26 that __init__ creates instance attributes when the object is first built. But instance attributes can also be created or modified in any method — though it is best practice to create all of them in __init__ so you always know what attributes an object has.

Here is a fuller example:

Python — BankAccount with three instance attributes
class BankAccount:

    def __init__(self, account_holder, starting_balance):
        self.holder = account_holder
        self.balance = starting_balance
        self.transaction_count = 0

Three instance attributes: holder, balance, and transaction_count. Every BankAccount object will have all three.

account_a = BankAccount("Kenji Watanabe", 500.0)
account_b = BankAccount("Amara Diallo", 1200.0)

After these two lines, account_a.holder is "Kenji Watanabe" and account_a.balance is 500.0, while account_b.holder is "Amara Diallo" and account_b.balance is 1200.0. The attributes belong to each object individually.

You can read instance attributes directly:

print(account_a.balance)   # 500.0
print(account_b.holder)    # Amara Diallo

You can also set them directly from outside the class:

account_a.balance = 600.0
Design Caution

Setting attributes directly from outside the class works in Python, but doing it frequently is usually a sign of poor design. Methods give you a controlled, tested way to change state. Direct attribute assignment bypasses any logic the class is meant to enforce. Use methods when the attribute change has rules attached to it.

Sections 27.3 – 27.5
What Methods Do: Reading, Changing, and Protecting State
Accessor Methods · Mutator Methods · Validation · State Transitions

Methods That Read State

Some methods exist only to report on an object's current state. They read attributes and return or display information. They do not change anything.

Python — BankAccount with read-only methods
class BankAccount:

    def __init__(self, account_holder, starting_balance):
        self.holder = account_holder
        self.balance = starting_balance
        self.transaction_count = 0

    def get_balance(self):
        return self.balance

    def get_holder(self):
        return self.holder

    def is_overdrawn(self):
        return self.balance < 0

    def summary(self):
        print(f"Account holder: {self.holder}")
        print(f"Balance: ${self.balance:.2f}")
        print(f"Transactions: {self.transaction_count}")

None of these methods change any attribute. They only report.

account = BankAccount("Kenji Watanabe", 500.0)
print(account.get_balance())     # 500.0
print(account.is_overdrawn())    # False
account.summary()
Output
500.0
False
Account holder: Kenji Watanabe
Balance: $500.00
Transactions: 0
Terminology

Methods that only read state are sometimes called accessor methods or getter methods in other programming languages. Python does not require those terms, but understanding the concept is useful: some methods observe, some methods change. Keeping them separate in your thinking makes classes easier to design and test.

Methods That Change State

Other methods modify attributes. These are the actions an object can perform. Each call moves the object to a new state.

Python — BankAccount with deposit and withdraw
class BankAccount:

    def __init__(self, account_holder, starting_balance):
        self.holder = account_holder
        self.balance = starting_balance
        self.transaction_count = 0

    def deposit(self, amount):
        self.balance = self.balance + amount
        self.transaction_count = self.transaction_count + 1

    def withdraw(self, amount):
        self.balance = self.balance - amount
        self.transaction_count = self.transaction_count + 1

    def get_balance(self):
        return self.balance

    def summary(self):
        print(f"Account holder: {self.holder}")
        print(f"Balance: ${self.balance:.2f}")
        print(f"Transactions: {self.transaction_count}")

Now trace this sequence:

account = BankAccount("Kenji Watanabe", 500.0)
account.deposit(200.0)
account.withdraw(75.0)
account.summary()

Tracing the State Transitions

StepActionbalancetransaction_count
After __init__Created500.00
After deposit(200.0)+200700.01
After withdraw(75.0)−75625.02
Output of summary()
Account holder: Kenji Watanabe
Balance: $625.00
Transactions: 2

Notice how each method call moves the object to a new state. The object remembers every change because it stores the current values in its own attributes between calls. This is the essential power of objects. A plain function cannot remember results between calls unless you pass data back and forth explicitly. An object holds its own data and updates it with every action.

Validation Inside Methods

Methods can contain logic that checks whether an action should be allowed before carrying it out. This is one of the main reasons to use methods instead of setting attributes directly. A bank account probably should not allow withdrawals larger than the current balance:

Python — withdraw with validation
def withdraw(self, amount):
    if amount <= 0:
        print("Withdrawal amount must be positive.")
    elif amount > self.balance:
        print("Insufficient funds.")
    else:
        self.balance = self.balance - amount
        self.transaction_count = self.transaction_count + 1

Now the method enforces a rule. You cannot accidentally set the balance to a negative number by calling withdraw() incorrectly. The logic lives in one place, inside the class.

Python — full BankAccount with validation
class BankAccount:

    def __init__(self, account_holder, starting_balance):
        self.holder = account_holder
        self.balance = starting_balance
        self.transaction_count = 0

    def deposit(self, amount):
        if amount <= 0:
            print("Deposit amount must be positive.")
        else:
            self.balance = self.balance + amount
            self.transaction_count = self.transaction_count + 1

    def withdraw(self, amount):
        if amount <= 0:
            print("Withdrawal amount must be positive.")
        elif amount > self.balance:
            print("Insufficient funds.")
        else:
            self.balance = self.balance - amount
            self.transaction_count = self.transaction_count + 1

    def get_balance(self):
        return self.balance

    def summary(self):
        print(f"Account holder: {self.holder}")
        print(f"Balance: ${self.balance:.2f}")
        print(f"Transactions: {self.transaction_count}")
Testing validation
account = BankAccount("Amara Diallo", 300.0)
account.deposit(-50)
account.withdraw(500.0)
account.withdraw(100.0)
account.summary()
Output
Deposit amount must be positive.
Insufficient funds.
Account holder: Amara Diallo
Balance: $200.00
Transactions: 1
Key Principle

When you write validation inside methods, you build an object that polices its own integrity. The failed deposit and failed withdrawal did not change the state. Only the valid withdrawal updated the balance and count. This is much safer than relying on the calling code to always pass correct values — which it won't, eventually.

Sections 27.6 – 27.8
Objects in the World: Interaction, Collections, and Representation
Object Interaction · Lists of Objects · __str__ · String Representation

Objects Interacting with Other Objects

Objects do not have to live in isolation. A method can receive another object as an argument, call methods on that object, or hold a reference to another object as an attribute.

Here is a Student class and a Classroom class that manages students:

Python — Student class
class Student:

    def __init__(self, name, grade):
        self.name = name
        self.grade = grade

    def is_passing(self):
        return self.grade >= 60

    def summary(self):
        status = "passing" if self.is_passing() else "failing"
        print(f"{self.name}: {self.grade} ({status})")
Python — Classroom class
class Classroom:

    def __init__(self, course_name):
        self.course = course_name
        self.students = []

    def add_student(self, student):
        self.students.append(student)

    def count_students(self):
        return len(self.students)

    def class_average(self):
        if len(self.students) == 0:
            return 0.0
        total = 0
        for student in self.students:
            total = total + student.grade
        return total / len(self.students)

    def print_roster(self):
        print(f"--- {self.course} ---")
        for student in self.students:
            student.summary()
        print(f"Class average: {self.class_average():.1f}")
Using both classes together
s1 = Student("Fatima Al-Rashid", 88)
s2 = Student("Lucas Moreau", 54)
s3 = Student("Yuna Park", 73)
s4 = Student("Dev Sharma", 91)

room = Classroom("Introduction to Programming")
room.add_student(s1)
room.add_student(s2)
room.add_student(s3)
room.add_student(s4)

room.print_roster()
Output
--- Introduction to Programming ---
Fatima Al-Rashid: 88 (passing)
Lucas Moreau: 54 (failing)
Yuna Park: 73 (passing)
Dev Sharma: 91 (passing)
Class average: 76.5
What to Notice
Three key observations about object interaction
  • ReferencesClassroom does not copy student data. It stores references to the Student objects. The same student object lives in both the variable s1 and inside the classroom's list.
  • Reading acrossThe class_average method reaches into each student object to read student.grade. One object's method is free to read the public attributes of another.
  • DelegationThe print_roster method calls each student's summary() method. Classroom delegates the display work to Student. Neither class needs to know the full internal logic of the other.

Lists of Objects

One of the most practical patterns you will use repeatedly is a list of objects. Many real programs manage collections of similar things: a list of students, a list of accounts, a list of products, a list of tasks.

Python — Task class and a list of tasks
class Task:

    def __init__(self, description, priority):
        self.description = description
        self.priority = priority
        self.completed = False

    def complete(self):
        self.completed = True

    def display(self):
        status = "Done" if self.completed else "Pending"
        print(f"[{status}] ({self.priority}) {self.description}")


tasks = []

tasks.append(Task("Buy groceries", "low"))
tasks.append(Task("Finish lab report", "high"))
tasks.append(Task("Reply to emails", "medium"))
tasks.append(Task("Review Chapter 27 notes", "high"))

# Mark one task complete
tasks[1].complete()

# Display all tasks
for task in tasks:
    task.display()
Output
[Pending] (low) Buy groceries
[Done] (high) Finish lab report
[Pending] (medium) Reply to emails
[Pending] (high) Review Chapter 27 notes

Working with lists of objects follows the same patterns as working with lists of integers or strings. You can iterate, filter, sort, and accumulate. The difference is that each element is an object with its own attributes and methods.

Python — filtering and counting with object lists
def find_high_priority(task_list):
    result = []
    for task in task_list:
        if task.priority == "high":
            result.append(task)
    return result

def count_pending(task_list):
    count = 0
    for task in task_list:
        if not task.completed:
            count = count + 1
    return count

urgent = find_high_priority(tasks)
print("High priority tasks:")
for task in urgent:
    task.display()

print(f"\nPending tasks: {count_pending(tasks)}")
Output
High priority tasks:
[Done] (high) Finish lab report
[Pending] (high) Review Chapter 27 notes

Pending tasks: 3

String Representations

When you print a plain object without any special setup, Python shows something like this:

<__main__.BankAccount object at 0x7f3c2a1b4d60>

This is not very helpful. It tells you the class name and a memory address, but nothing about the object's actual state. Python gives you a way to fix this: the __str__ method. When Python needs to convert an object to a string — for printing or for use in an f-string — it calls __str__ if you have defined one.

Python — BankAccount with __str__
class BankAccount:

    def __init__(self, account_holder, starting_balance):
        self.holder = account_holder
        self.balance = starting_balance
        self.transaction_count = 0

    def deposit(self, amount):
        if amount > 0:
            self.balance = self.balance + amount
            self.transaction_count = self.transaction_count + 1

    def withdraw(self, amount):
        if 0 < amount <= self.balance:
            self.balance = self.balance - amount
            self.transaction_count = self.transaction_count + 1

    def __str__(self):
        return f"BankAccount({self.holder}, ${self.balance:.2f})"


account = BankAccount("Kenji Watanabe", 500.0)
account.deposit(100.0)
print(account)
print(f"Account details: {account}")
Output
BankAccount(Kenji Watanabe, $600.00)
Account details: BankAccount(Kenji Watanabe, $600.00)

__str__ is one of Python's special methods, also called dunder methods because of the double underscores on each side of the name. You have already used __init__. __str__ is the most immediately useful beginner one after it.

When you have a list of objects, printing the list without __str__ gives you a list of memory addresses. With __str__, you get readable information:

accounts = [
    BankAccount("Kenji Watanabe", 500.0),
    BankAccount("Amara Diallo", 1200.0),
    BankAccount("Priya Nair", 75.0),
]

for account in accounts:
    print(account)
Output
BankAccount(Kenji Watanabe, $500.00)
BankAccount(Amara Diallo, $1200.00)
BankAccount(Priya Nair, $75.00)
Sections 27.9 – 27.10
Choosing and Designing: Dictionaries, Classes, and Clear Behavior
Dictionary vs. Object · When to Use Each · Design Principles · Single Responsibility

Dictionaries Versus Objects

At this stage, you have two tools for grouping related data: dictionaries and objects. Understanding when to use each one will improve your program designs.

Consider storing information about a product. Here is the dictionary approach:

Python — dictionary approach
product = {
    "name": "Spiral Notebook",
    "price": 3.49,
    "quantity": 50
}

print(product["name"])
product["quantity"] = product["quantity"] - 1

def is_low_stock(p):
    return p["quantity"] < 10

print(is_low_stock(product))

Here is the class approach:

Python — class approach
class Product:

    def __init__(self, name, price, quantity):
        self.name = name
        self.price = price
        self.quantity = quantity

    def sell_one(self):
        if self.quantity > 0:
            self.quantity = self.quantity - 1
        else:
            print(f"Cannot sell {self.name}: out of stock.")

    def restock(self, amount):
        self.quantity = self.quantity + amount

    def is_low_stock(self):
        return self.quantity < 10

    def __str__(self):
        return f"Product({self.name}, ${self.price:.2f}, qty={self.quantity})"


product = Product("Spiral Notebook", 3.49, 50)
product.sell_one()
print(product)
print(product.is_low_stock())
Output
Product(Spiral Notebook, $3.49, qty=49)
False
Use a Dictionary When
Data is simple and flat
No behavior is attached to the data. You are building a quick lookup table, counting occurrences, or mapping keys to values. The data will be passed around and read in many places with no logic attached. Data arrives from files or APIs as key-value records.
Use a Class When
Data and behavior belong together
You need to enforce rules about how data changes. You will create many similar objects, each maintaining its own state. The program becomes clearer when you can write account.deposit(100) instead of update_account(account, "deposit", 100).
A Useful Test

If you find yourself writing many functions that all take a dictionary as their first argument and do something to it, that is a sign a class might be cleaner. The dictionary plus its functions is already acting like an object — you just have not given it a class yet. Neither approach is always right. Real programs often use both. A class might hold a dictionary inside it as an attribute. A function might return a list of objects.

Designing Clear Object Behavior

Writing a class is not only about knowing the syntax. It is about designing something that will be easy to use correctly and hard to use incorrectly. Here are guidelines for designing clear object behavior.

Design Principles
Six guidelines for clear class design
  • One responsibilityA BankAccount should manage its own balance. It should not also manage customer addresses, send emails, or parse CSV files. Keep each class focused on one thing. This is the single responsibility principle applied to objects.
  • All attributes in __init__Do not create new instance attributes in random methods throughout the class. If you look at __init__ and cannot see all the attributes the object will have, the class is harder to understand and debug. Every attribute the object needs should be visible in one place.
  • Methods for state changesUse methods to change state, not direct attribute assignment from outside the class. The more an object controls its own state changes, the more you can trust its state is valid.
  • Write __str__It costs almost nothing and makes testing, debugging, and printing vastly more pleasant. Do it for every class you write.
  • Small focused methodsA method that does three unrelated things is harder to test, understand, and reuse than three methods that each do one thing. If a method is getting long, look for natural sub-tasks to break out.
  • Test each method independentlyBefore connecting objects together, make sure each individual method works correctly on its own. Create a small object, call one method, and check whether the state matches what you expected.
Python — poor design: attribute created outside __init__
# Confusing: attributes appear unpredictably
class Task:
    def __init__(self, description):
        self.description = description

    def set_priority(self, p):
        self.priority = p   # appears here for the first time — hard to find
Python — clear design: all attributes in __init__
# Clear: all attributes declared upfront
class Task:
    def __init__(self, description):
        self.description = description
        self.completed = False
        self.priority = "medium"

A Complete InventoryItem Class

Here is a class built from scratch, following all the principles from this chapter.

Worked Example 27.1
Planning the class

Goal: Represent a single item in a store inventory.

  • Attributes: name, unit price, quantity in stock
  • Actions: restock, sell, apply discount, check low stock
  • Display: readable __str__
Python — complete InventoryItem class
class InventoryItem:
    """Represents a single product in a store inventory."""

    def __init__(self, name, unit_price, quantity):
        self.name = name
        self.unit_price = unit_price
        self.quantity = quantity

    def restock(self, amount):
        """Add units to inventory."""
        if amount <= 0:
            print("Restock amount must be positive.")
        else:
            self.quantity = self.quantity + amount
            print(f"Restocked {self.name}. New quantity: {self.quantity}")

    def sell(self, units):
        """Remove units from inventory if available."""
        if units <= 0:
            print("Sale quantity must be positive.")
        elif units > self.quantity:
            print(f"Not enough {self.name} in stock. Available: {self.quantity}")
        else:
            self.quantity = self.quantity - units
            total = units * self.unit_price
            print(f"Sold {units} x {self.name} for ${total:.2f}.")

    def apply_discount(self, percent):
        """Reduce unit price by a percentage (0 to 100)."""
        if percent <= 0 or percent >= 100:
            print("Discount must be between 0 and 100 percent.")
        else:
            reduction = self.unit_price * (percent / 100)
            self.unit_price = self.unit_price - reduction
            print(f"New price of {self.name}: ${self.unit_price:.2f}")

    def is_low_stock(self, threshold=10):
        """Return True if quantity is below the threshold."""
        return self.quantity < threshold

    def total_value(self):
        """Return the total value of all units in stock."""
        return self.quantity * self.unit_price

    def __str__(self):
        return (f"InventoryItem({self.name}, "
                f"${self.unit_price:.2f}/unit, "
                f"qty={self.quantity})")
Using the class
item = InventoryItem("USB Cable", 8.99, 25)
print(item)

item.sell(5)
item.sell(30)
item.restock(20)
item.apply_discount(10)
print(item)
print(f"Total stock value: ${item.total_value():.2f}")
print(f"Low stock? {item.is_low_stock()}")
Output
InventoryItem(USB Cable, $8.99/unit, qty=25)
Sold 5 x USB Cable for $44.95.
Not enough USB Cable in stock. Available: 20
Restocked USB Cable. New quantity: 40
New price of USB Cable: $8.09
InventoryItem(USB Cable, $8.09/unit, qty=40)
Total stock value: $323.60
Low stock? False

Tracing the State Changes

StepActionunit_pricequantity
After __init__Created8.9925
After sell(5)−5 units sold8.9920
After failed sell(30)No change — insufficient stock8.9920
After restock(20)+20 units8.9940
After apply_discount(10)Price reduced 10%8.0940
What to Notice

Every state transition is predictable, because every method either validates before acting or reports the problem clearly. The failed sell(30) left the object completely unchanged. The caller knows what happened from the printed message. The object's integrity was never compromised.

Chapter Summary

Key Terms

object state
The collection of values stored in an object's instance attributes at a given moment. State changes whenever a method modifies an attribute.
instance attribute
A variable attached to a specific object, created with self.attribute_name inside the class, carrying a separate value for each instance.
accessor method
A method that reads and returns information about an object's state without changing any attribute. Sometimes called a getter method in other languages.
mutator method
A method that changes one or more of an object's instance attributes, moving the object to a new state.
state transition
The change in an object's attribute values caused by calling a method that modifies them. Each mutator call is a state transition.
__str__ method
A special method that defines how an object is converted to a readable string, called automatically by print() and f-strings.
dunder method
Any of Python's special methods with double underscores on each side of the name, such as __init__ and __str__. Short for "double underscore."
validation
Logic inside a method that checks whether an operation is allowed before carrying it out, protecting the object's integrity.
list of objects
A list where each element is an instance of a class. Iterated and processed using the same loop and filter patterns as any other list.
single responsibility
A design principle stating that each class should be responsible for one focused area of the program, not a mixture of unrelated concerns.
Back MatterCode Reading

Read each code block carefully and answer the questions without running the code.

Exercise 1

class Counter:

    def __init__(self, label):
        self.label = label
        self.count = 0

    def increment(self):
        self.count = self.count + 1

    def reset(self):
        self.count = 0

    def value(self):
        return self.count

    def __str__(self):
        return f"{self.label}: {self.count}"


c = Counter("Clicks")
c.increment()
c.increment()
c.increment()
c.reset()
c.increment()
print(c)
  1. What are the instance attributes of Counter?
  2. What is the value of c.count after the three increment() calls?
  3. What does reset() do to the state?
  4. What does the final print(c) display?

Exercise 2

class Rectangle:

    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

    def perimeter(self):
        return 2 * (self.width + self.height)

    def scale(self, factor):
        self.width = self.width * factor
        self.height = self.height * factor

    def __str__(self):
        return f"Rectangle({self.width} x {self.height})"


r = Rectangle(4, 6)
print(r.area())
r.scale(2)
print(r.perimeter())
print(r)
  1. What does r.area() return before scale is called?
  2. After r.scale(2), what are the new values of r.width and r.height?
  3. What does r.perimeter() return after scaling?
  4. What does the final print(r) display?

Exercise 3

class Jar:

    def __init__(self, capacity):
        self.capacity = capacity
        self.contents = 0

    def fill(self, amount):
        space = self.capacity - self.contents
        added = min(amount, space)
        self.contents = self.contents + added

    def empty(self):
        self.contents = 0

    def is_full(self):
        return self.contents == self.capacity

    def __str__(self):
        return f"Jar({self.contents}/{self.capacity})"


j = Jar(10)
j.fill(7)
j.fill(5)
print(j)
print(j.is_full())
j.empty()
print(j)
  1. After j.fill(7), what is j.contents?
  2. When j.fill(5) is called, only 3 units can actually be added. Explain why.
  3. What does the first print(j) display?
  4. What does the second print(j) display after j.empty()?
Back MatterCode Tracing

Exercise 4

Trace through the following code step by step. For each method call, write the values of all instance attributes after the call completes.

class ScoreTracker:

    def __init__(self, player_name):
        self.player = player_name
        self.score = 0
        self.high_score = 0

    def add_points(self, points):
        if points > 0:
            self.score = self.score + points
            if self.score > self.high_score:
                self.high_score = self.score

    def lose_points(self, points):
        if points > 0:
            self.score = self.score - points
            if self.score < 0:
                self.score = 0

    def reset(self):
        self.score = 0

    def __str__(self):
        return f"{self.player} | Score: {self.score} | Best: {self.high_score}"


tracker = ScoreTracker("Maya")
tracker.add_points(50)
tracker.add_points(30)
tracker.lose_points(20)
tracker.add_points(60)
tracker.lose_points(200)
tracker.add_points(15)
print(tracker)

Fill in this tracing table, then write the final output:

After stepscorehigh_score
__init__  
add_points(50)  
add_points(30)  
lose_points(20)  
add_points(60)  
lose_points(200)  
add_points(15)  

Exercise 5

class LightSwitch:

    def __init__(self):
        self.on = False
        self.times_switched = 0

    def toggle(self):
        self.on = not self.on
        self.times_switched = self.times_switched + 1

    def turn_on(self):
        if not self.on:
            self.toggle()

    def turn_off(self):
        if self.on:
            self.toggle()

    def __str__(self):
        state = "ON" if self.on else "OFF"
        return f"Switch is {state}, toggled {self.times_switched} times"


switch = LightSwitch()
switch.turn_on()
switch.turn_on()
switch.toggle()
switch.turn_off()
switch.toggle()
print(switch)
  1. Trace the values of switch.on and switch.times_switched after each call.
  2. What is the final output of print(switch)?
  3. Why does the second turn_on() not increment times_switched?
Back MatterPredict the Output

Exercise 6

class Wallet:

    def __init__(self, owner):
        self.owner = owner
        self.cash = 0.0

    def add_cash(self, amount):
        self.cash = self.cash + amount

    def spend(self, amount):
        if amount > self.cash:
            print("Not enough cash.")
        else:
            self.cash = self.cash - amount

    def __str__(self):
        return f"{self.owner}'s wallet: ${self.cash:.2f}"


w = Wallet("Jordan")
w.add_cash(20.0)
w.add_cash(15.0)
w.spend(8.50)
w.spend(30.0)
print(w)
  1. What does the program print? Write the exact output.
  2. What is w.cash after all calls complete?

Exercise 7

class Dog:

    def __init__(self, name, breed):
        self.name = name
        self.breed = breed
        self.tricks = []

    def learn(self, trick):
        self.tricks.append(trick)

    def show_off(self):
        if len(self.tricks) == 0:
            print(f"{self.name} doesn't know any tricks yet.")
        else:
            print(f"{self.name} knows: {', '.join(self.tricks)}")

    def __str__(self):
        return f"Dog({self.name}, {self.breed})"


d1 = Dog("Rex", "Labrador")
d2 = Dog("Mochi", "Shiba Inu")

d1.learn("sit")
d1.learn("shake")
d2.learn("spin")

d1.show_off()
d2.show_off()
print(d1)
print(d2)
  1. Write the exact output of this program line by line.
  2. Do d1 and d2 share the same tricks list? Explain why or why not.
Back MatterDebugging and Error Analysis

Exercise 8 — Find all the errors

class Temperature:

    def __init__(celsius):
        self.celsius = celsius

    def to_fahrenheit():
        return self.celsius * 9/5 + 32

    def raise_by(self, degrees):
        celsius = celsius + degrees

    def __str__(self):
        return f"{celsius}°C"


temp = Temperature(20)
temp.raise_by(5)
print(temp.to_fahrenheit())
print(temp)
  1. There are at least four errors in this class. List each error, explain what is wrong, and write a corrected version of the class.
Hint

Check every method signature. Check every use of self. Check every reference to an attribute — is it using self.attribute_name or just a plain name?

Exercise 9 — Find the logic error

class SavingsGoal:

    def __init__(self, goal_name, target_amount):
        self.name = goal_name
        self.target = target_amount
        self.saved = 0.0

    def deposit(self, amount):
        self.saved = self.saved + amount

    def is_reached(self):
        return self.saved > self.target   # Is this correct?

    def remaining(self):
        return self.target - self.saved

    def __str__(self):
        return (f"Goal: {self.name} | "
                f"Saved: ${self.saved:.2f} / ${self.target:.2f}")


goal = SavingsGoal("New Laptop", 800.0)
goal.deposit(800.0)
print(goal.is_reached())   # Should print True
  1. What does the code currently print?
  2. Explain the bug in is_reached.
  3. Write the corrected version of is_reached.

Exercise 10 — Find the edge case failure

class GradeBook:

    def __init__(self, student_name):
        self.name = student_name
        self.scores = []
        self.total = 0

    def add_score(self, score):
        self.scores.append(score)
        self.total = self.total + score

    def average(self):
        return self.total / len(self.scores)

    def highest(self):
        return max(self.scores)

    def __str__(self):
        return f"{self.name}: avg={self.average():.1f}"


gb = GradeBook("Yuna Park")
print(gb.average())   # This line will cause a problem
  1. What error will occur when gb.average() is called on an empty gradebook? Name the error type.
  2. Write a corrected version of average() that handles the empty case gracefully.
  3. Does highest() have the same problem? Explain.
Back MatterShort Programming Tasks
  1. Timer class. Write a Timer class with a name and an elapsed time starting at zero. Methods: tick(seconds) — add seconds (reject negatives); reset() — set elapsed back to zero; elapsed() — return current elapsed time; __str__ — display name and elapsed time. Test it by creating a timer, advancing it several times, displaying it, and resetting it.
  2. Playlist class. Write a Playlist class with a name and a list of song titles. Methods: add_song(title); remove_song(title) — remove if found, print a message if not; song_count(); display() — print all songs numbered from 1; __str__ — show name and song count. Add four songs, remove one that exists and one that does not, then display the result.
  3. VendingMachine class. Write a VendingMachine class that holds a list of items (each a dictionary with "name", "price", and "quantity"), and a total of money inserted. Methods: insert_money(amount); select_item(name) — dispense if available and funds are sufficient, otherwise print a clear message; display_items(); __str__ — show total money inserted.
  4. find_most_expensive function. Write a function find_most_expensive(items) that takes a list of InventoryItem objects (from the worked example) and returns the one with the highest unit price. If the list is empty, return None. Test it with at least five items.
Mini-ProjectClass-Based Contact Book
Project Overview

In Chapter 18, you built a contact book using dictionaries. This project rebuilds it using a class-based design. The goal is not just to get the same result — it is to see how classes make the design clearer and safer.

Part A: Design a Contact class

A Contact should hold a name, a phone number, an email address, and a category (such as "friend", "work", or "family").

  1. Implement update_phone(new_number) and update_email(new_email) methods.
  2. Implement matches(search_term) that returns True if the search term appears anywhere in the name, phone, or email (case-insensitive).
  3. Write a __str__ method that displays all four fields neatly.

Part B: Design a ContactBook class

A ContactBook should hold a name (for example, "Personal Contacts") and a list of Contact objects.

  1. Implement add_contact(contact), remove_contact(name), count(), and display_all().
  2. Implement search(term) that returns a list of all contacts where matches(term) is True.
  3. Implement list_by_category(category) that returns all contacts in that category.
  4. Write a __str__ that shows the book name and contact count.

Part C: Build and test the application

  1. Create a ContactBook and add at least six contacts with varied names, numbers, emails, and categories.
  2. Search for a term that matches at least two contacts. Print the results.
  3. List contacts by one category. Print the results.
  4. Update one contact's phone number, then print that contact to verify the change.
  5. Remove one contact, then display the full book to confirm it is gone.
Reflection Questions
Write your answers as comments at the top of your program file
  • Question 1What is the main advantage of using a Contact class rather than a dictionary for each contact?
  • Question 2Where does validation belong: in Contact, in ContactBook, or in both? Why?
  • Question 3How does __str__ make this program easier to test and debug compared to the dictionary version?
Back MatterMixed Review

These questions draw on skills from Chapters 11 through 26.

  1. A function receives a list of objects and must return only those where a certain attribute exceeds a threshold. Write this function for BankAccount objects, returning accounts whose balance is above a given amount. Explain how this pattern is identical to filtering a list of numbers.
  2. Write a function transfer(source, destination, amount) that takes two BankAccount objects and an amount, withdraws from the source, and deposits into the destination. What should the function do if the source does not have enough funds? Show the complete code and an example of using it.
  3. You have a list of Task objects. Write a function that returns three values: the count of completed tasks, the count of pending tasks, and the number of high-priority pending tasks. Show how you would call it and print all three results.
  4. A Student object has a grade attribute. You have a list of students. Without using any built-in sorting function, write a simple loop that finds the student with the highest grade and the student with the lowest grade. Print both students' names and grades.
  5. Explain the difference between print(account) and print(account.get_balance()) when account is a BankAccount object. When would you use each one? Under what circumstances might print(account) fail to show useful information?
Chapter 27 Checklist
Confirm your understanding before moving to Chapter 28
Concepts
  • I can define object state and explain how it changes over time.
  • I can read a class definition and identify all instance attributes.
  • I understand the difference between methods that read state and methods that change state.
  • I can explain why validation inside methods protects object integrity.
  • I can explain what the __str__ method does and why it is useful.
  • I can describe when a dictionary is the right tool and when a class is the right tool.
Skills
  • I can trace a sequence of method calls and record the state of attributes after each one.
  • I can write a class with a complete __init__, read-only methods, mutating methods with validation, and __str__.
  • I can create a list of objects, iterate over it, filter it, and accumulate from it.
  • I can write two cooperating classes where one holds a list of instances of the other.
  • I can identify and fix errors where self is missing from method signatures or attribute accesses.
Design Judgment
  • I create all instance attributes in __init__, not scattered through other methods.
  • I write small, focused methods rather than large methods that do many things.
  • I test each method individually with small examples before composing objects together.
Preview — Chapter 28
Designing Small Applications

Chapter 28 expands from individual classes to complete small applications. You will plan a program from requirements, design its data structures and flow, combine functions and classes, add persistence by saving data to files, and think about testing the whole program — not just individual methods. The skills you practiced in Chapters 26 and 27 are the foundation. Chapter 28 shows how to assemble them into something a real user could open and run.

MGU Library Connection
  • Programming with Python — Appendix K: Object-Oriented Programming Reference
  • Programming with Python — Appendix L: Testing and Refactoring Reference
  • Companion resource: Object State Tracing Worksheet
  • Companion resource: Dictionaries vs. Objects Decision Guide
  • Companion resource: Class Design Checklist
  • Next chapter: Chapter 28 — Designing Small Applications