Practical Projects and Exercises Combining Concepts

Lesson Overview

This lesson ties everything together with a practical project: a Banking System. You will build a complete system from scratch using classes, objects, encapsulation, inheritance, and abstraction. This project simulates a real-world library where you can add books, register members, lend books, and handle returns, demonstrating how OOP principles work together in a real application.

Lesson Content

OOP Banking System: From Basics to Professional Application

Project Overview

In this comprehensive project, you'll build a professional Banking System using all the Object-Oriented Programming concepts you've learned. This isn't just theory—this is how real banking applications are structured in production systems.​

By the end of this lesson, you'll have created a fully functional banking system that demonstrates:

  • Abstract Base Classes and Interfaces
  • Inheritance and Method Overriding
  • Encapsulation with Private Variables
  • Getter and Setter Methods with Validation
  • Instance, Class, and Static Methods
  • Polymorphism in Action

Why This Project Matters

Banks handle millions of transactions daily. Their systems must be secure, consistent, and maintainable. OOP provides the architectural foundation that makes this possible​

Real-World Connection:

  • All Banks use OOP principles in their core banking systems
  • Payment integrators rely on abstract interfaces to integrate multiple payment methods
  • ATM systems use encapsulation to protect your account data

The Challenge: Managing Multiple Account Types

Imagine you're the CTO of a new bank. You need to support three different account types:

  • Savings Account: Has minimum balance requirements and earns interest

  • Current Account: Designed for businesses, allows overdrafts, no interest

Each account type has different rules but shares common features like deposits, withdrawals, and balance updates. How do you organize this code so that:

  1. 50 developers can work on different account types without breaking each other's code

  2. New account types (like Credit Cards or Loans) can be added easily

  3. Core banking operations remain consistent across all accounts

  4. Sensitive data like balance and PIN are protected from unauthorized access

This is where OOP principles become your architectural foundation.


Step 1: The Solution: Abstract Base Classes

Solution: Create an Abstract Base Class that says: "Every account type MUST have these exact methods, or Python won't let you create objects."

The Architecture at a Glance

BankAccount (Abstract Base Class)
├── Private Variables: __balance, __pin, __account_number                # Private variables that adds security, need to use setter and getter methods to access or update data
├── Public Variables: account_holder_name, account_creation_date         # Public Variables
├── Abstract Methods: calculate_interest(), withdraw()                   # Methods that are must needeed when implementing a Child Classes, these methods cannot be ingnored 
├── Concrete Methods: deposit(), get_balance(), set_pin()                # Instance Methods that add functionlity/featrues
├── Class Methods: get_total_accounts(), get_bank_name()                 # Methods that are based on Class, but not based on Instance   
└── Static Methods: calculate_tds()                                      # Standalone method that used to do some calcualtions.   

Code Implementation:

from abc import ABC, abstractmethod
from datetime import datetime
import random

# Abstract Base Class - The Contract
class BankAccount(ABC):
    """
    Blueprint for all bank accounts.
    Every account type must implement these methods.
    """
    
    # Class variable - shared across all accounts
    total_accounts = 0
    bank_name = "ByLearning Bank of India"
    
    def __init__(self, account_holder_name, initial_deposit):
        # Private variables (encapsulation)
        self.__account_number = self._generate_account_number()            # Calls a private method to generate account number
        self.__balance = 0
        self.__pin = None
        
        # Public variables
        self.account_holder_name = account_holder_name
        self.account_creation_date = datetime.now().strftime("%Y-%m-%d")
        
        # Increment class variable
        BankAccount.total_accounts += 1
        
        # Initial deposit
        self.deposit(initial_deposit)                                    # calls a method to deposit money
    
    # Private method (helper method)
    def _generate_account_number(self):
        """Generates unique 10-digit account number"""
        return random.randint(1000000000, 9999999999)
    
    # Abstract methods - MUST be implemented by child classes
    @abstractmethod
    def calculate_interest(self):
        """Each account type calculates interest differently"""
        pass
    
    @abstractmethod
    def withdraw(self, amount):
        """Each account type has different withdrawal rules"""
        pass
    
    # Concrete methods - inherited by all child classes
    def deposit(self, amount):
        """Common deposit logic for all accounts"""
        if amount <= 0:
            print("Deposit amount must be positive.")
            return False
        
        self.__balance += amount
        print(f"₹{amount} deposited successfully.")
        return True
    
    # Getter methods
    def get_balance(self):
        """Safe way to view balance"""
        return self.__balance
    
    def get_account_number(self):
        """Safe way to view account number"""
        return self.__account_number
    
    # Setter method with validation
    def set_pin(self, new_pin):
        """Set a 4-digit PIN"""
        if len(str(new_pin)) != 4 or not str(new_pin).isdigit():
            print("PIN must be exactly 4 digits.")
            return False
        self.__pin = new_pin
        print("PIN set successfully.")
        return True
    
    def verify_pin(self, pin):
        """Verify PIN without exposing it"""
        return self.__pin == pin
    
    # Class method - works on the class, not instances
    @classmethod
    def get_total_accounts(cls):
        """Returns total accounts created across all types"""
        return cls.total_accounts
    
    @classmethod
    def get_bank_name(cls):
        """Returns the bank's name"""
        return cls.bank_name
    
    # Static methods - utility functions
    @staticmethod
    def calculate_tds(interest_amount):
        """Calculates 10% TDS on interest earned"""
        return interest_amount * 0.10

Step 2: Savings Account - Inheritance & Method Overriding

Real-World Rules:

  • Minimum balance: ₹1,000
  • Interest rate: 4% per annum
  • Daily withdrawal limit: ₹50,000

Code Implementation:

class SavingsAccount(BankAccount):
    """
    Savings account with minimum balance requirement
    """
    
    # Class variables specific to savings accounts
    minimum_balance = 1000
    interest_rate = 0.04  # 4%
    daily_withdrawal_limit = 50000
    
    def __init__(self, account_holder_name, initial_deposit):
        if initial_deposit < self.minimum_balance:
            raise ValueError(f"Initial deposit must be at least ₹{self.minimum_balance}")
        
        super().__init__(account_holder_name, initial_deposit)                    # Initating the exiting variables, methods on Abstract class
        self.account_type = "Bylearning Savings Account"
        self.__daily_withdrawn_today = 0
    
    # Overriding abstract method
    def calculate_interest(self):
        """Calculate 4% interest on current balance"""
        balance = self.get_balance()
        interest = balance * self.interest_rate
        
        # Deduct TDS
        tds = BankAccount.calculate_tds(interest)
        net_interest = interest - tds
        
        print(f"Interest Earned: ₹{interest:.2f}")
        print(f"TDS Deducted (10%): ₹{tds:.2f}")
        print(f"Net Interest Credited: ₹{net_interest:.2f}")
        
        self.deposit(net_interest)
        return net_interest
    
    # Overriding abstract method
    def withdraw(self, amount, pin):
        """Withdraw with minimum balance and daily limit checks"""
        
        # PIN verification
        if not self.verify_pin(pin):
            print("Incorrect PIN.")
            return False
        
        # Validation checks
        if amount <= 0:
            print("Withdrawal amount must be positive.")
            return False
        
        balance = self.get_balance()
        
        # Check minimum balance
        if balance - amount < self.minimum_balance:
            print(f"Cannot withdraw. Minimum balance of ₹{self.minimum_balance} must be maintained.")
            return False
        
        # Check daily limit
        if self.__daily_withdrawn_today + amount > self.daily_withdrawal_limit:
            print(f"Daily withdrawal limit of ₹{self.daily_withdrawal_limit} exceeded.")
            return False
        
        # Process withdrawal
        self._BankAccount__balance -= amount
        self.__daily_withdrawn_today += amount
        
        
        print(f"₹{amount} withdrawn successfully.")
        print(f"Daily withdrawal used: ₹{self.__daily_withdrawn_today}/{self.daily_withdrawal_limit}")
        return True
What Just Happened?

We created a concrete class that inherits from our abstract BankAccount blueprint.we're implementing the actual business rules that govern how a savings account works in real banks.

class SavingsAccount(BankAccount): 

This single line tells Python: "SavingsAccount is a specialized type of BankAccount. Give it all the parent's methods and variables, plus its own unique features."

What gets inherited automatically:

  • deposit() method
  • get_balance() and get_account_number() getters
  • set_pin() and verify_pin() methods
  • Class variables like total_accounts and bank_name
  • Private variables like __balance, __pin, __account_number
super().__init__(account_holder_name, initial_deposit) 

Translation: "Hey parent class (BankAccount), run your initialization code first. Set up the account number, balance, PIN, and increment the total accounts counter."

This is crucial because:

  • We don't repeat the code for generating account numbers
  • We reuse the validated deposit logic
  • We maintain the total accounts counter automatically

Real-world analogy: When you join a company, HR sets up your employee ID, email, and benefits (parent initialization). Then your specific department (child class) assigns your desk, team, and project-specific tools.


Step 3: Current Account - Different Rules, Same Interface

Real-World Rules:

  • No minimum balance
  • No interest earned
  • Overdraft facility: Can go negative up to ₹10,000
  • No withdrawal limit

Code Implementation:

class CurrentAccount(BankAccount):
    """
    Current account with overdraft facility
    """
    
    overdraft_limit = 10000
    interest_rate = 0.0  # No interest
    
    def __init__(self, account_holder_name, initial_deposit):
        super().__init__(account_holder_name, initial_deposit)
        self.account_type = "Bylearning Current Account"
    
    # Overriding abstract method
    def calculate_interest(self):
        """Current accounts don't earn interest"""
        print("Current accounts do not earn interest.")
        return 0
    
    # Overriding abstract method - Different logic than Savings
    def withdraw(self, amount, pin):
        """Withdraw with overdraft facility"""
        
        if not self.verify_pin(pin):
            print("Incorrect PIN.")
            return False
        
        if amount <= 0:
            print("Withdrawal amount must be positive.")
            return False
        
        balance = self.get_balance()
        
        # Check overdraft limit
        if balance - amount < -self.overdraft_limit:
            print(f"Overdraft limit of ₹{self.overdraft_limit} exceeded.")
            return False
        
        # Process withdrawal
        self._BankAccount__balance -= amount
        
        print(f"₹{amount} withdrawn successfully.")
        
        if self._BankAccount__balance < 0:
            print(f"Overdraft used: ₹{abs(self._BankAccount__balance)}/{self.overdraft_limit}")
        
        return True

Step 4: Polymorphism in Action

The Power of a Common Interface

Because all account types inherit from BankAccount, you can write functions that work with any account type​

def transfer_money(from_account , to_account,  amount, pin):
    """Transfer money between any two accounts"""
    print(f"Transfer Initiated")
    print(f"From: {from_account.account_holder_name}")
    print(f"To: {to_account.account_holder_name}")
    
    # Withdraw from source
    if from_account.withdraw(amount, pin):
        # Deposit to destination
        to_account.deposit(amount)
        print(f"Transfer of ₹{amount} completed successfully!")
        return True
    else:
        print("Transfer failed.")
        return False

Step 5: Testing the Complete System

# Create different account types
priya_savings = SavingsAccount("Priya Singh", 5000)
priya_savings.set_pin(1234)

ravi_current = CurrentAccount("Ravi Sharma", 10000)
ravi_current.set_pin(5678)

# Display bank info using class method
print(f"Welcome to {BankAccount.get_bank_name()}")
print(f"Total Accounts: {BankAccount.get_total_accounts()}\n")

# Test deposits
priya_savings.deposit(2000)

# Test withdrawals (different rules for each type)
priya_savings.withdraw(1000, 1234)  # Checks minimum balance
ravi_current.withdraw(15000, 5678)  # Uses overdraft

# Polymorphism: Test transfer
transfer_money(ajay_savings, ravi_current, 500, 1234)

Key Takeaways

ConceptWhere UsedWhy Important
AbstractionBankAccount abstract classForces consistency across 50 developers
EncapsulationPrivate __balance, __pinPrevents unauthorized direct access
InheritanceAll accounts inherit from BankAccountReuses common code (deposit, statements)
Method OverridingEach account overrides withdraw()Different accounts, different rules
Polymorphismtransfer_money() functionOne function works with all account types
Class Methodsget_total_accounts()Tracks bank-wide statistics
Static Methodscalculate_tds()Utility functions, no object needed
Getter/Setterget_balance(), set_pin()Controlled access with validation

Real-World Extension Ideas

  1. Customer Class: One person can have multiple accounts
  2. Loan System: Add LoanAccount class with EMI calculation
  3. Credit Card: Add CreditCardAccount with billing cycles
  4. Database Integration: Save accounts to SQLite/PostgreSQL
  5. Web Interface: Connect to Django for online banking
  6. Transaction Limits: Add daily/monthly spending limits
  7. Beneficiary Management: Save frequent transfer recipients
  8. Notifications: SMS/Email alerts for transactions

This project demonstrates how OOP principles are not academic concepts—they're the backbone of every production banking system you use daily.

 

Topics: python