Exercises: Object-Oriented Programming — Thinking in Objects

These exercises progress from basic class definitions through design challenges. By the end, you should feel confident defining classes, creating objects, and deciding when OOP is the right approach.

Difficulty Guide: - ⭐ Foundational (5-10 min each) - ⭐⭐ Intermediate (10-20 min each) - ⭐⭐⭐ Challenging (20-40 min each) - ⭐⭐⭐⭐ Advanced/Research (40+ min each)


Part A: Conceptual Understanding ⭐

A.1. In your own words, explain the difference between a class and an object. Use an analogy that is NOT the cookie cutter or blueprint analogy from the chapter.

A.2. Explain what self does in a Python class. Why is it the first parameter of every method? What happens if you forget it?

A.3. What is the difference between an instance attribute and a class attribute? Give an example of when you would use each.

A.4. Explain the difference between __str__ and __repr__. When does Python use each one?

A.5. Why is it dangerous to use a mutable object (like a list) as a class attribute? What should you do instead?

A.6. What does @property do? Why would you use it instead of just making an attribute public?


Part B: Basic Class Definitions ⭐⭐

B.1. Define a Book class with the following: - Attributes: title, author, pages, current_page (starts at 0) - Methods: read(num_pages) — advances current_page by num_pages (but not past the end), progress() — returns percentage read as a float - __str__ that shows the title, author, and reading progress

Test with:

book = Book("Dune", "Frank Herbert", 412)
book.read(100)
print(book)           # Should show progress
print(f"{book.progress():.1f}%")  # ~24.3%
book.read(500)        # Should cap at 412
print(f"Page {book.current_page} of {book.pages}")  # Page 412 of 412

B.2. Define a Rectangle class with: - Attributes: width, height - Methods: area(), perimeter(), is_square() (returns True if width equals height) - __str__ that shows dimensions (e.g., "Rectangle(5x3)") - __eq__ that checks if two rectangles have the same area

Test with:

r1 = Rectangle(5, 3)
r2 = Rectangle(3, 5)
r3 = Rectangle(15, 1)
print(r1)                    # Rectangle(5x3)
print(f"Area: {r1.area()}")  # Area: 15
print(r1 == r3)              # True (same area)
print(r1.is_square())        # False

B.3. Define a Counter class with: - An __init__ that starts the count at 0 - Methods: increment(), decrement() (can't go below 0), reset() - A value property that returns the current count - __str__ that shows the count

Test with:

c = Counter()
c.increment()
c.increment()
c.increment()
c.decrement()
print(c)          # Count: 2
print(c.value)    # 2
c.reset()
print(c.value)    # 0

B.4. Define a Playlist class with: - Attributes: name, songs (list of strings, starts empty) - Methods: add_song(title), remove_song(title), shuffle() (use random.shuffle), total_songs() (returns count) - __str__ that shows the playlist name and number of songs - __len__ that returns the number of songs

Test with:

import random
random.seed(42)
p = Playlist("Road Trip")
p.add_song("Bohemian Rhapsody")
p.add_song("Hotel California")
p.add_song("Sweet Child O' Mine")
print(p)          # Road Trip (3 songs)
print(len(p))     # 3
p.shuffle()
for song in p.songs:
    print(f"  - {song}")

B.5. Define a Stopwatch class that tracks elapsed time in seconds: - Methods: start(), stop(), reset(), elapsed() (returns seconds as float) - Use time.time() to track real time - Prevent calling start() when already running or stop() when not running (raise RuntimeError)

Test with:

import time
sw = Stopwatch()
sw.start()
time.sleep(0.5)
sw.stop()
print(f"Elapsed: {sw.elapsed():.1f}s")  # Approximately 0.5s
sw.reset()
print(f"After reset: {sw.elapsed():.1f}s")  # 0.0s

Part C: Special Methods and Encapsulation ⭐⭐-⭐⭐⭐

C.1. Define a Money class that stores an amount in cents (to avoid floating-point issues): - __init__(self, dollars, cents=0) — stores as total cents internally - Properties: dollars (returns whole dollar amount), cents (returns remaining cents), total_cents (returns raw internal value) - __str__ returns formatted like "$12.50" - __repr__ returns "Money(12, 50)" - __eq__ compares by total cents - __lt__ for sorting - __add__ so you can write Money(5, 50) + Money(3, 75) and get Money(9, 25)

Test with:

a = Money(5, 50)
b = Money(3, 75)
c = a + b
print(c)            # $9.25
print(repr(c))      # Money(9, 25)
print(a < b)        # False
print(a == Money(5, 50))  # True
prices = [Money(3, 99), Money(1, 50), Money(12, 0)]
for p in sorted(prices):
    print(p)        # $1.50, $3.99, $12.00

C.2. Define a Password class that stores a password securely (well, more securely than plain text): - __init__ takes a password string and stores its hash using hashlib.sha256 - check(attempt) returns True if the hash of the attempt matches - __str__ returns "********" (never reveals the password) - __repr__ returns "Password(*****)" - Attempting to access the stored hash via a public attribute should not be possible (use name mangling)

Test with:

pw = Password("hunter2")
print(pw)                    # ********
print(pw.check("hunter2"))   # True
print(pw.check("wrong"))     # False
print(repr(pw))              # Password(*****)

C.3. Define a CircularBuffer class (a fixed-size buffer that overwrites the oldest data when full): - __init__(self, capacity) — creates a buffer with the given maximum size - append(item) — adds an item (overwrites oldest if full) - __len__ — returns number of items currently in the buffer - __getitem__ — supports indexing (0 is oldest) - __str__ — shows contents like "CircularBuffer([1, 2, 3])" - Use @property for is_full and capacity

Test with:

buf = CircularBuffer(3)
buf.append(1)
buf.append(2)
buf.append(3)
print(buf)            # CircularBuffer([1, 2, 3])
print(buf.is_full)    # True
buf.append(4)         # Overwrites 1
print(buf)            # CircularBuffer([2, 3, 4])
print(buf[0])         # 2 (oldest)
print(len(buf))       # 3

C.4. Define an EmailAddress class that validates and parses email addresses: - __init__ takes a string and validates it has exactly one @ with non-empty parts on both sides and a . in the domain - Properties: user (part before @), domain (part after @) - __str__ returns the email as a string - __eq__ compares case-insensitively - Raises ValueError if the email is invalid

Test with:

email = EmailAddress("Alice@Example.COM")
print(email)            # alice@example.com (stored lowercase)
print(email.user)       # alice
print(email.domain)     # example.com
print(email == EmailAddress("ALICE@EXAMPLE.COM"))  # True

try:
    bad = EmailAddress("not-an-email")
except ValueError as e:
    print(f"Error: {e}")

Part D: Design and Analysis ⭐⭐⭐

D.1. Procedural to OOP Conversion. The following procedural code manages a simple shopping cart. Refactor it into a Product class and a ShoppingCart class:

# Procedural version
def create_product(name, price):
    return {"name": name, "price": price}

def create_cart():
    return {"items": [], "discount": 0}

def add_to_cart(cart, product, quantity=1):
    cart["items"].append({"product": product, "quantity": quantity})

def get_total(cart):
    total = sum(
        item["product"]["price"] * item["quantity"]
        for item in cart["items"]
    )
    return total * (1 - cart["discount"])

def apply_discount(cart, percent):
    cart["discount"] = percent / 100

# Usage
milk = create_product("Milk", 3.99)
bread = create_product("Bread", 2.50)
cart = create_cart()
add_to_cart(cart, milk, 2)
add_to_cart(cart, bread)
apply_discount(cart, 10)
print(f"Total: ${get_total(cart):.2f}")

Your OOP version should support the same operations with a cleaner interface. Include __str__ for both classes and __len__ for the cart.

D.2. Design a Deck of Cards. Create Card and Deck classes: - A Card has a rank (2-10, J, Q, K, A) and a suit (hearts, diamonds, clubs, spades) - Card.__str__ shows something like "Ace of Spades" or "10 of Hearts" - Card.__lt__ orders by rank (2 lowest, Ace highest), breaking ties by suit alphabetically - A Deck starts with 52 cards, has shuffle(), deal() (returns top card), and __len__ - Deck.deal() raises a ValueError if the deck is empty

Test by creating a deck, shuffling, and dealing 5 cards.

D.3. Text Adventure Room System. Using the Room class from the chapter, create at least 5 interconnected rooms for a mini dungeon. Then create a Player class that can: - Move between rooms by direction ("north", "south", etc.) - Pick up and drop items - Show current location and inventory Write a simple game loop that lets a user explore the dungeon.

D.4. Contact Book. Design and implement a Contact class and a ContactBook class: - Contact stores: name, phone, email (all validated) - ContactBook supports: add, delete, search by name (partial match), list all (sorted), import/export to JSON - Include appropriate special methods for both classes


Part E: Challenge Problems ⭐⭐⭐⭐

E.1. Fraction Class. Implement a Fraction class that: - Stores numerator and denominator (auto-reduces using math.gcd) - Supports +, -, *, / with other fractions via __add__, __sub__, __mul__, __truediv__ - Supports ==, <, > comparisons - __str__ shows "3/4", and __float__ converts to decimal - Raises ZeroDivisionError for zero denominator - Handles negative numbers correctly (sign always on numerator)

Test with:

a = Fraction(1, 2)
b = Fraction(1, 3)
print(a + b)        # 5/6
print(a * b)        # 1/6
print(a > b)        # True
print(float(a))     # 0.5
print(Fraction(4, 6))  # 2/3 (auto-reduced)

E.2. Vector2D Class. Implement a Vector2D class for 2D mathematical vectors: - __init__(self, x, y) - magnitude() returns the length - normalize() returns a unit vector - dot(other) returns the dot product - __add__, __sub__, __mul__ (scalar multiplication), __eq__ - __str__ shows "Vector2D(3.0, 4.0)"

Test with:

v1 = Vector2D(3, 4)
v2 = Vector2D(1, 2)
print(v1.magnitude())   # 5.0
print(v1 + v2)          # Vector2D(4.0, 6.0)
print(v1 * 2)           # Vector2D(6.0, 8.0)
print(v1.dot(v2))       # 11.0
n = v1.normalize()
print(f"{n.magnitude():.1f}")  # 1.0

E.3. State Machine. Implement a StateMachine class that can model a traffic light or a vending machine: - add_state(name) adds a valid state - add_transition(from_state, event, to_state) adds a valid transition - trigger(event) moves to the next state (raises ValueError if transition not valid) - current_state property shows where you are - history property shows all states visited

Model a traffic light that cycles: GREEN --(timer)--> YELLOW --(timer)--> RED --(timer)--> GREEN.

E.4. Linked List from Scratch. Implement Node and LinkedList classes: - Node has data and next_node attributes - LinkedList supports: append(data), prepend(data), delete(data), find(data), __len__, __str__, __iter__ - This is a preview of Chapter 20 (Stacks and Queues) — see how far you can get with what you know now.


Part F: Reflection ⭐

F.1. Look back at a program you wrote earlier in this course (from any chapter). Identify at least two places where an OOP approach would have improved the design. Explain why.

F.2. Explain the threshold concept from this chapter ("objects as bundled state + behavior") to someone who has only used procedural programming. Use a concrete example from your own life.