Unit 6 • Lesson 10

OOP in Real Projects

Overview

This final topic brings everything together with hands-on examples like banking systems, games, or inventory trackers. You'll learn how combining classes, inheritance, and encapsulation helps build robust, real-world applications, demonstrating the power of OOP in practice.

Intermediate 30–40 min

What You Will Learn in This Lesson

By the end of this lesson, you will know:

  • Designing with OOP: How to structure real-world problems using classes and objects.
  • Building complete systems: Create working applications that combine multiple OOP concepts.
  • Best practices: Learn how to organize code, use inheritance effectively, and apply encapsulation.
  • Real-world examples: Build a banking system, a simple game, and an inventory tracker.
  • Putting it all together: See how classes, inheritance, polymorphism, and encapsulation work together.

Project 1: Banking System

Let's build a simple banking system that demonstrates encapsulation, inheritance, and method overriding. This system will have different types of accounts with different behaviors.

Complete Banking System

class BankAccount:
    def __init__(self, account_number, balance=0):
        self.__account_number = account_number  # Private attribute
        self.__balance = balance  # Private attribute
    
    def deposit(self, amount):
        if amount > 0:
            self.__balance += amount
            return f"Deposited ${amount}. New balance: ${self.__balance}"
        return "Invalid deposit amount"
    
    def withdraw(self, amount):
        if amount > 0 and amount <= self.__balance:
            self.__balance -= amount
            return f"Withdrew ${amount}. New balance: ${self.__balance}"
        return "Insufficient funds or invalid amount"
    
    def get_balance(self):
        return self.__balance
    
    def get_account_number(self):
        return self.__account_number

class SavingsAccount(BankAccount):
    def __init__(self, account_number, balance=0, interest_rate=0.02):
        super().__init__(account_number, balance)
        self.__interest_rate = interest_rate
    
    def add_interest(self):
        interest = self.get_balance() * self.__interest_rate
        self.deposit(interest)
        return f"Interest added: ${interest:.2f}"
    
    def withdraw(self, amount):
        if amount > self.get_balance() * 0.9:  # Can't withdraw more than 90%
            return "Cannot withdraw more than 90% of balance"
        return super().withdraw(amount)

class CheckingAccount(BankAccount):
    def __init__(self, account_number, balance=0, overdraft_limit=100):
        super().__init__(account_number, balance)
        self.__overdraft_limit = overdraft_limit
    
    def withdraw(self, amount):
        available = self.get_balance() + self.__overdraft_limit
        if amount <= available:
            self._BankAccount__balance -= amount  # Accessing private attribute
            return f"Withdrew ${amount}. Balance: ${self.get_balance()}"
        return "Exceeds overdraft limit"

# Usage
savings = SavingsAccount("SAV001", 1000, 0.03)
checking = CheckingAccount("CHK001", 500, 200)

print(savings.deposit(200))
print(savings.add_interest())
print(checking.withdraw(600))  # Uses overdraft
print(f"Savings balance: ${savings.get_balance()}")

Key OOP Concepts Used

  • Encapsulation: Private attributes (__balance, __account_number) protect data.
  • Inheritance: SavingsAccount and CheckingAccount inherit from BankAccount.
  • Polymorphism: Both subclasses override withdraw() with different behaviors.
  • Method overriding: Each account type has its own withdrawal rules.

Try It Yourself

Run the banking system code above and experiment with creating accounts, making deposits, and withdrawals:

Press Run to see output

Project 2: Simple Game System

Let's create a simple game system with different character types. This demonstrates inheritance, polymorphism, and how to structure a game using OOP.

Game Character System

class Character:
    def __init__(self, name, health=100):
        self.name = name
        self.health = health
        self.max_health = health
    
    def attack(self):
        return 10  # Base attack damage
    
    def take_damage(self, damage):
        self.health -= damage
        if self.health < 0:
            self.health = 0
        return f"{self.name} took {damage} damage. Health: {self.health}/{self.max_health}"
    
    def heal(self, amount):
        self.health += amount
        if self.health > self.max_health:
            self.health = self.max_health
        return f"{self.name} healed {amount}. Health: {self.health}/{self.max_health}"
    
    def is_alive(self):
        return self.health > 0
    
    def __str__(self):
        return f"{self.name} (Health: {self.health}/{self.max_health})"

class Warrior(Character):
    def __init__(self, name, health=120):
        super().__init__(name, health)
        self.armor = 5
    
    def attack(self):
        return 15  # Warriors hit harder
    
    def take_damage(self, damage):
        actual_damage = max(0, damage - self.armor)  # Armor reduces damage
        return super().take_damage(actual_damage)

class Mage(Character):
    def __init__(self, name, health=80):
        super().__init__(name, health)
        self.mana = 100
    
    def attack(self):
        if self.mana >= 20:
            self.mana -= 20
            return 25  # Mages hit hardest but use mana
        return 5  # Weak attack without mana
    
    def cast_spell(self):
        if self.mana >= 30:
            self.mana -= 30
            return 30  # Powerful spell
        return 0

class Archer(Character):
    def __init__(self, name, health=90):
        super().__init__(name, health)
        self.arrows = 10
    
    def attack(self):
        if self.arrows > 0:
            self.arrows -= 1
            return 12
        return 3  # Weak melee attack without arrows
    
    def restock_arrows(self, amount):
        self.arrows += amount
        return f"{self.name} restocked {amount} arrows. Total: {self.arrows}"

# Game simulation
warrior = Warrior("Conan")
mage = Mage("Gandalf")
archer = Archer("Legolas")

characters = [warrior, mage, archer]

for char in characters:
    print(char)
    print(f"  Attack damage: {char.attack()}")
    print(char.take_damage(20))
    print()

OOP Benefits in Game Design

  • Reusability: Common character behavior is defined once in the base class.
  • Extensibility: Easy to add new character types (Rogue, Paladin, etc.).
  • Polymorphism: All characters can be treated the same way, but behave differently.
  • Maintainability: Changes to base behavior automatically affect all characters.

Try It Yourself

Create a simple battle between two characters:

Press Run to see output

Project 3: Inventory Management System

An inventory system demonstrates how to manage collections of objects, use class methods, and apply OOP principles to real-world data management.

Complete Inventory System

class Product:
    total_products = 0  # Class variable
    
    def __init__(self, name, price, quantity=0):
        self.name = name
        self.price = price
        self.quantity = quantity
        Product.total_products += 1
        self.product_id = Product.total_products
    
    def update_quantity(self, change):
        self.quantity += change
        if self.quantity < 0:
            self.quantity = 0
    
    def get_value(self):
        return self.price * self.quantity
    
    def __str__(self):
        return f"{self.name} (ID: {self.product_id}) - ${self.price:.2f} x {self.quantity}"
    
    @classmethod
    def get_total_products(cls):
        return cls.total_products

class Inventory:
    def __init__(self):
        self.products = {}  # Dictionary: product_id -> Product
    
    def add_product(self, product):
        self.products[product.product_id] = product
        return f"Added {product.name} to inventory"
    
    def remove_product(self, product_id):
        if product_id in self.products:
            product = self.products.pop(product_id)
            return f"Removed {product.name} from inventory"
        return "Product not found"
    
    def find_product(self, product_id):
        return self.products.get(product_id)
    
    def get_total_value(self):
        total = sum(product.get_value() for product in self.products.values())
        return total
    
    def list_products(self):
        if not self.products:
            return "Inventory is empty"
        result = "Inventory:\n"
        for product in self.products.values():
            result += f"  {product}\n"
        return result
    
    def low_stock_items(self, threshold=5):
        low_stock = [p for p in self.products.values() if p.quantity <= threshold]
        return low_stock

# Usage
inventory = Inventory()

# Add products
laptop = Product("Laptop", 999.99, 10)
mouse = Product("Mouse", 29.99, 25)
keyboard = Product("Keyboard", 79.99, 3)

inventory.add_product(laptop)
inventory.add_product(mouse)
inventory.add_product(keyboard)

print(inventory.list_products())
print(f"Total inventory value: ${inventory.get_total_value():.2f}")
print(f"Total products in system: {Product.get_total_products()}")

# Check low stock
low_stock = inventory.low_stock_items(threshold=5)
print("\nLow stock items:")
for item in low_stock:
    print(f"  {item.name}: {item.quantity} remaining")

OOP Concepts in Inventory System

  • Class variables: total_products tracks all products created.
  • Class methods: get_total_products() works with the class, not instances.
  • Composition: Inventory contains multiple Product objects.
  • Data management: Dictionary stores products for efficient lookup.

Try It Yourself

Create your own inventory and add some products:

Press Run to see output

Best Practices for OOP Projects

When building real-world projects with OOP, keep these principles in mind:

1. Single Responsibility Principle

Each class should have one clear purpose. A BankAccount handles banking operations, not user authentication or email notifications.

2. Use Inheritance Wisely

Only use inheritance when there's a true "is-a" relationship. A Dog is an Animal, but a Car is not a Vehicle if you're building a transportation app — it might be better to use composition instead.

3. Encapsulate Data

Use private attributes (__attribute) to protect important data. Provide getter and setter methods when external access is needed.

4. Keep Methods Focused

Each method should do one thing well. If a method is doing too much, break it into smaller methods.

5. Use Polymorphism

Design your code so that different objects can be used interchangeably. This makes your code more flexible and easier to extend.

Putting It All Together

Real-world applications combine all these OOP concepts:

  • Classes define the structure of your data and behavior.
  • Objects represent specific instances with their own data.
  • Inheritance allows you to reuse and extend existing code.
  • Polymorphism lets different objects respond to the same interface.
  • Encapsulation protects data and controls access.
  • super() helps you extend parent functionality without duplication.

Remember

OOP is a tool to help you organize code and solve problems. Don't force OOP where it doesn't fit — sometimes a simple function is better than a class. But when you're dealing with multiple related pieces of data and behavior, OOP shines!

End-of-Lesson Exercises

Complete these exercises to practice building real-world OOP systems:

Exercise 1: Library System

Create a library system with a Book class and a Library class. Books should have title, author, and ISBN. The Library should be able to add books, remove books, find books by title, and list all books. Use encapsulation to protect the book collection.

Write your code above and click "Check Answer" to verify it's correct.

Exercise 2: Shape Hierarchy

Create a Shape base class with an area() method. Create Rectangle and Circle subclasses that override area() with their specific calculations. Create a list of different shapes and calculate the total area using polymorphism.

Write your code above and click "Check Answer" to verify it's correct.