IS 4010: Application Development with Artificial Intelligence

Week 06: Object-oriented programming

Brandon M. Greenwell

Object-oriented programming

Session 1: from functions to objects


When functions aren’t enough: a shopping cart story

The problem: managing complex state with functions

  • Scenario: Building an e-commerce shopping cart system
  • Challenge: Multiple pieces of related data (items, totals, discounts, tax rates)
  • Function approach: Becomes unwieldy as complexity grows
  • Design pressure: Related state may be passed repeatedly, updated inconsistently, or duplicated

A function-based shopping cart

# Creating and managing cart state with functions - gets messy fast!
def create_cart():
    return {"items": [], "subtotal": 0, "tax_rate": 0.08, "discount": 0}

def add_item(cart, name, price, quantity=1):
    cart["items"].append({"name": name, "price": price, "qty": quantity})
    cart["subtotal"] += price * quantity
    return cart

def apply_discount(cart, discount_percent):
    cart["discount"] = discount_percent
    return cart

def calculate_total(cart):
    discounted = cart["subtotal"] * (1 - cart["discount"]/100)
    return discounted * (1 + cart["tax_rate"])

# Usage becomes cumbersome and error-prone
cart = create_cart()
cart = add_item(cart, "Laptop", 999.99)
cart = add_item(cart, "Mouse", 29.99)
cart = apply_discount(cart, 10)
total = calculate_total(cart)
print(f"Total: ${total:.2f}")

Your turn: feel the pain

Try to add more functionality to the function-based cart: 1. Add a function to remove items 2. Add a function to change item quantities 3. Notice how many parameters you need to pass around!

# Your code here - try adding remove_item() and change_quantity() functions
# Notice how complex the parameter passing becomes!

def remove_item(cart, name):
    """Remove an item from the cart."""
    for item in cart["items"]:
        if item["name"] == name:
            cart["subtotal"] -= item["price"] * item["qty"]
            cart["items"].remove(item)
            break
    return cart

def change_quantity(cart, name, quantity):
    """Change the quantity of an item in the cart."""
    for item in cart["items"]:
        if item["name"] == name:
            cart["subtotal"] -= item["price"] * item["qty"]
            item["qty"] = quantity
            cart["subtotal"] += item["price"] * item["qty"]
            break
    return cart

A class-based shopping cart

class ShoppingCart:
    """Represents a shopping cart with items, discounts, and tax calculation."""

    def __init__(self, tax_rate=0.08):
        self.items = []
        self.tax_rate = tax_rate
        self.discount_percent = 0

    def add_item(self, name, price, quantity=1):
        """Add an item to the cart."""
        self.items.append({"name": name, "price": price, "qty": quantity})

    def apply_discount(self, discount_percent):
        """Apply a discount to the entire cart."""
        self.discount_percent = discount_percent

    @property
    def subtotal(self):
        """Calculate subtotal before discount and tax."""
        return sum(item["price"] * item["qty"] for item in self.items)

    @property
    def total(self):
        """Calculate final total after discount and tax."""
        discounted = self.subtotal * (1 - self.discount_percent/100)
        return discounted * (1 + self.tax_rate)

# Usage is clean, intuitive, and maintainable
cart = ShoppingCart()
cart.add_item("Laptop", 999.99)
cart.add_item("Mouse", 29.99)
cart.apply_discount(10)
print(f"Total: ${cart.total:.2f}")

Your turn: extend the class

Add methods to the ShoppingCart class: 1. remove_item(name) - remove an item by name 2. clear() - empty the cart 3. item_count - property that returns total number of items

class ShoppingCart:
    """Represents a shopping cart with items, discounts, and tax calculation."""

    def __init__(self, tax_rate=0.08):
        """Initialize a new shopping cart."""
        self.items = []
        self.tax_rate = tax_rate
        self.discount_percent = 0

    def add_item(self, name, price, quantity=1):
        """Add an item to the cart."""
        self.items.append({"name": name, "price": price, "qty": quantity})

    def remove_item(self, name):
        """Remove an item from the cart by name."""
        # Keep only items that don't match the name
        self.items = [item for item in self.items if item["name"] != name]

    def clear(self):
        """Empty the cart."""
        self.items = []

    def apply_discount(self, discount_percent):
        """Apply a discount to the entire cart."""
        self.discount_percent = discount_percent

    @property
    def item_count(self):
        """Return total number of items in the cart."""
        return sum(item["qty"] for item in self.items)
    
    @property
    def subtotal(self):
        """Calculate subtotal before discount and tax."""
        return sum(item["price"] * item["qty"] for item in self.items)

    @property
    def total(self):
        """Calculate final total after discount and tax."""
        discounted = self.subtotal * (1 - self.discount_percent/100)
        return discounted * (1 + self.tax_rate)

    def __str__(self):
        """Return a string representation of the cart."""
        lines = ["Shopping Cart:"]
        if not self.items:
            lines.append("  (Empty)")
        else:
            for item in self.items:
                lines.append(f"  {item['name']} x{item['qty']} - ${item['price']:.2f} each")
            lines.append(f"  Subtotal: ${self.subtotal:.2f}")
            if self.discount_percent > 0:
                lines.append(f"  Discount: {self.discount_percent}%")
            lines.append(f"  Total: ${self.total:.2f}")
            lines.append(f"  Item Count: {self.item_count}")
            
        return "\n".join(lines)

# Test the enhanced class
cart = ShoppingCart()
cart.add_item("Laptop", 999.99)
cart.add_item("Mouse", 29.99, 2)
print("--- Initial Cart ---")
print(cart)

cart.remove_item("Mouse")
print("\n--- After removing Mouse ---")
print(cart)

cart.clear()
print("\n--- After clearing ---")
print(cart)

When object-oriented programming helps

  • Encapsulation: Bundle data and methods together, hide implementation details
  • Code reusability: Write once, use everywhere - create multiple cart instances
  • Maintainability: Changes to internal implementation don’t break external code
  • Collaboration: Team members can work on different classes independently
  • Framework examples: Django, Flask, and FastAPI all provide class-based APIs

What is a class?

  • A class is a blueprint or template for creating objects
  • An object (or instance) is a specific item created from that class blueprint
  • Attributes: The data that belongs to an object (like variables)
  • Methods: The functions that operate on an object’s data
  • Analogy: A Car class is the blueprint; your specific Honda Civic is an object instance

The __init__ method: object initialization

  • The __init__ method is a special constructor function
  • Initialization hook: Runs after Python creates a new instance
  • Purpose: Initialize the object’s attributes with starting values
  • The self parameter: References the specific instance being created
  • Convention: Always the first parameter in instance methods
class BankAccount:
    """Represents a bank account with balance tracking."""

    def __init__(self, account_holder: str, initial_balance: float = 0.0):
        # Instance attributes - unique to each account
        self.account_holder = account_holder
        self.balance = initial_balance
        self.transaction_history = []

    def deposit(self, amount: float):
        """Add money to the account."""
        self.balance += amount
        self.transaction_history.append(f"Deposited ${amount:.2f}")

# Create specific account instances
alice_account = BankAccount("Alice Johnson", 1000.0)
bob_account = BankAccount("Bob Smith")  # Uses default balance of 0.0

Your turn: bank account features

Enhance the BankAccount class: 1. Add a get_transaction_history() method 2. Add an account_number attribute (you can use a simple counter) 3. Add a minimum balance requirement

# Your enhanced BankAccount class here

The __str__ method: human-readable representation

  • The __str__ method defines how objects appear when printed
  • Automatic invocation: Called by print(), str(), and string formatting
  • User-friendly: Should return meaningful information for end users
  • AI exercise: Ask for a draft, then verify its attributes and exact string output
  • Debugging aid: A useful representation makes object state easier to inspect
class User:
    """Represents a user in a social media application."""

    def __init__(self, username: str, email: str, join_date: str):
        self.username = username
        self.email = email
        self.join_date = join_date
        self.followers = 0
        self.is_verified = False

    def __str__(self) -> str:
        """Return a user-friendly string representation."""
        verification = "✓" if self.is_verified else ""
        return f"@{self.username}{verification} ({self.followers} followers) - Joined {self.join_date}"

# Create and display user instances
user1 = User("grace_hopper", "grace@example.com", "2023-01-15")
user2 = User("ada_lovelace", "ada@example.com", "2023-02-20")
user2.is_verified = True
user2.followers = 50000

print(user1)  # @grace_hopper (0 followers) - Joined 2023-01-15
print(user2)  # @ada_lovelace✓ (50000 followers) - Joined 2023-02-20

Your turn: social media features

Extend the User class with: 1. A like_post(user, post_index) method 2. A get_recent_posts(count=5) method 3. A bio attribute and update_bio(new_bio) method

# Your enhanced User class here

Introducing Lab 06: part 1

  • Deliverable: Create the specified Book class in week06/lab06.py
  • AI collaboration: Ask for an explanation or review, then verify every attribute and method against the tests
  • Core concepts: Practice __init__ constructors and __str__ representations
  • Modeling: Connect the lab’s fields and methods to the class contract
  • Foundation: Prepare for inheritance in part 2

Session 2: methods and inheritance


Giving objects behavior with methods

  • Methods are functions defined inside a class that operate on object data
  • Instance methods: Most common type, always take self as first parameter
  • Behavior modeling: Define an object’s operations alongside its data
  • Encapsulation: Methods can access and modify private object state safely
  • State management: Methods ensure object data stays consistent and valid
class GameCharacter:
    """Represents a character in a role-playing game."""

    def __init__(self, name: str, health: int = 100):
        self.name = name
        self.health = health
        self.max_health = health
        self.experience = 0
        self.level = 1

    def take_damage(self, damage: int):
        """Reduce character health, ensuring it doesn't go below 0."""
        self.health = max(0, self.health - damage)
        print(f"{self.name} takes {damage} damage! Health: {self.health}/{self.max_health}")

    def heal(self, amount: int):
        """Restore character health, capped at maximum."""
        old_health = self.health
        self.health = min(self.max_health, self.health + amount)
        healed = self.health - old_health
        print(f"{self.name} heals for {healed} points! Health: {self.health}/{self.max_health}")

    def gain_experience(self, exp: int):
        """Add experience and level up if threshold is reached."""
        self.experience += exp
        if self.experience >= self.level * 100:  # Simple leveling formula
            self.level += 1
            self.max_health += 20
            self.health = self.max_health  # Full heal on level up
            print(f"{self.name} reached level {self.level}!")

# Create and interact with a character
hero = GameCharacter("Aria the Brave")
hero.take_damage(30)
hero.heal(15)
hero.gain_experience(150)

Your turn: game character enhancement

Add these features to the GameCharacter class: 1. A use_potion() method that heals based on inventory 2. A magic_attack(target) method with different damage 3. A get_inventory_value() method that calculates total value

# Your enhanced GameCharacter class here

Documenting classes and methods

  • The rules from Week 05 carry over. A docstring is a string literal in triple quotes, placed as the first statement
  • Class docstring: say what the object represents, not how it is built
  • Method docstring: same as any function. Say what it returns or what it changes
  • Skip the obvious: __init__ rarely needs more than the class docstring already tells the reader
  • Lab 06 hands you get_age with its docstring written. Match that style for the methods you add
class Book:
    """A book in a personal library."""

    def __init__(self, title: str, author: str, year: int):
        self.title = title
        self.author = author
        self.year = year

    def get_age(self):
        """Return the number of years since publication."""
        return date.today().year - self.year

Understanding inheritance: reusing behavior

  • Inheritance allows classes to inherit attributes and methods from parent classes
  • Code reuse: Write common functionality once, share across multiple related classes
  • “Is-a” relationships: Child classes are specialized versions of parent classes
  • Hierarchical design: Can model genuine subtype relationships
  • Design trade-off: Inheritance can reuse behavior, but composition is often simpler

Implementing inheritance: the super() function

  • Syntax: class ChildClass(ParentClass): establishes inheritance relationship
  • super() function: Calls methods from the parent class
  • Initializer reuse: A child initializer can call the parent initializer with super() when it needs the parent’s setup
  • Method override: Child classes can replace parent methods with specialized versions
  • Method extension: Or extend parent methods with additional functionality
class Vehicle:
    """Base class for all vehicles."""

    def __init__(self, make: str, model: str, year: int):
        self.make = make
        self.model = model
        self.year = year
        self.mileage = 0

    def start_engine(self):
        """Start the vehicle's engine."""
        print(f"The {self.year} {self.make} {self.model} engine starts.")

    def drive(self, miles: float):
        """Drive the vehicle and update mileage."""
        self.mileage += miles
        print(f"Drove {miles} miles. Total mileage: {self.mileage}")

class ElectricCar(Vehicle):
    """Electric vehicle with battery management."""

    def __init__(self, make: str, model: str, year: int, battery_capacity: float):
        super().__init__(make, model, year)  # Call parent constructor
        self.battery_capacity = battery_capacity
        self.battery_level = 100.0  # Start fully charged

    def start_engine(self):
        """Override: Electric cars don't have traditional engines."""
        print(f"The {self.year} {self.make} {self.model} powers on silently.")

    def charge(self, hours: float):
        """Charge the battery (unique to electric cars)."""
        charge_added = min(hours * 10, 100 - self.battery_level)
        self.battery_level += charge_added
        print(f"Charged for {hours} hours. Battery: {self.battery_level:.1f}%")

# Inheritance in action
tesla = ElectricCar("Tesla", "Model S", 2023, 100.0)
tesla.start_engine()  # Uses overridden method
tesla.drive(50)       # Uses inherited method
tesla.charge(2)       # Uses unique method

Your turn: vehicle inheritance

Create a new vehicle type: 1. Create a Motorcycle class that inherits from Vehicle 2. Override the start_engine() method with a motorcycle-specific message 3. Add a wheelie() method unique to motorcycles 4. Make motorcycles more fuel-efficient in the drive() method

# Your Motorcycle class here

Advanced OOP concepts preview

  • Class variables: Shared data across all instances of a class
  • Properties: Computed attributes using @property decorator
  • Class methods: Methods that operate on the class itself, not instances
  • Static methods: Utility functions that belong logically to the class
  • Multiple inheritance: Inheriting from multiple parent classes (advanced topic)
class Product:
    """Represents a product in an inventory system."""

    # Class variable - shared across all instances
    total_products_created = 0

    def __init__(self, name: str, price: float):
        self.name = name
        self._price = price  # Private attribute (convention)
        Product.total_products_created += 1

    @property
    def price(self) -> float:
        """Get the product price."""
        return self._price

    @price.setter
    def price(self, value: float):
        """Set the product price with validation."""
        if value < 0:
            raise ValueError("Price cannot be negative")
        self._price = value

    @classmethod
    def get_total_products(cls) -> int:
        """Return total number of products created."""
        return cls.total_products_created

    @staticmethod
    def calculate_tax(price: float, tax_rate: float = 0.08) -> float:
        """Calculate tax amount for a given price."""
        return price * tax_rate

# Advanced features in action
laptop = Product("Gaming Laptop", 1299.99)
mouse = Product("Wireless Mouse", 79.99)

print(f"Total products: {Product.get_total_products()}")  # Class method
print(f"Tax on laptop: ${Product.calculate_tax(laptop.price):.2f}")  # Static method

# Property with validation
# laptop.price = -100 # Would raise ValueError
laptop.price = 1199.99  # Valid price change

Your turn: advanced features

Enhance the Product class: 1. Add a stock_quantity attribute and property with validation 2. Create a @classmethod called create_electronics(name, price) that sets category automatically 3. Add a @staticmethod for calculating bulk discount rates

# Your enhanced Product class here

Where OOP can help

  • Web development: Django models, FastAPI dependencies
  • Game development: Characters, items, game states, collision systems
  • Financial systems: Accounts, transactions, portfolios, risk calculations
  • Data science: Custom data structures, machine learning pipelines
  • Desktop applications: UI components, event handling, application state

Computer science pioneers: OOP

  • Alan Kay (1940-): Coined “object-oriented programming,” created Smalltalk
  • Kristen Nygaard (1926-2002): Co-invented Simula, the first OOP language
  • Ole-Johan Dahl (1931-2002): Co-creator of Simula, Turing Award winner
  • Legacy: Their work influenced later object-oriented languages and interfaces
  • Design idea: Objects combine state with behavior exposed through methods

Introducing Lab 06: part 2

  • Build on Part 1: Add get_age(), which returns the years since publication
  • Current year: Use date.today().year, not a hardcoded year; the tests derive the expected age the same way
  • Inheritance practice: Create an EBook class that inherits from Book
  • Method override: Customize __str__ in the child class; it must include the file size and MB
  • Required skills: Reuse the parent initializer and inherit get_age without redefining it
  • AI collaboration: Use AI assistants to explore different implementation approaches

Your turn: complete Lab 06

Create your own implementation of the Book and EBook classes following the exact lab requirements: 1. Implement the Book class with the specified attributes and methods 2. Implement the EBook class that inherits from Book 3. Test both classes thoroughly 4. Try adding additional features like a AudioBook class

# Your complete Lab 06 implementation here
# This is your chance to practice everything you've learned!

if __name__ == '__main__':
    # Test your classes here
    pass

Looking ahead: OOP in practice

  • Next week: Working with data - APIs, JSON, and object serialization
  • Project applications: Your final projects will benefit from OOP design
  • Further study: Explore composition, protocols, and selected design patterns
  • AI enhancement: Use AI tools to refine your OOP designs and implementations

Summary: key takeaways

  • OOP motivation: Can give related state and behavior a clear interface
  • Core concepts: Classes (blueprints), objects (instances), attributes (data), methods (behavior)
  • Common methods: __init__ initializes instances; __str__ supplies a human-readable representation when useful
  • Inheritance: Enables code reuse and hierarchical relationships
  • Design judgment: Use a class when it gives related state and behavior a clearer interface

Time for Lab 06

Deliverable: week06/lab06.py containing Book and EBook.

  • Book stores title, author, and year, and its __str__ includes all three
  • get_age() returns the years since publication, using date.today().year
  • EBook extends Book with file_size, reuses the parent initializer via super(), and inherits get_age
  • Do not copy or modify the supplied tests
uv run --directory week06 python -m pytest tests/ -v
git add week06/lab06.py
git commit -m "Complete Lab 06"
git push origin main

A green Week 06 badge earns the complete 10 points.

Instructions: week06/lab06.md