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:
50 developers can work on different account types without breaking each other's code
New account types (like Credit Cards or Loans) can be added easily
Core banking operations remain consistent across all accounts
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()methodget_balance()andget_account_number()gettersset_pin()andverify_pin()methods- Class variables like
total_accountsandbank_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
| Concept | Where Used | Why Important |
| Abstraction | BankAccount abstract class | Forces consistency across 50 developers |
| Encapsulation | Private __balance, __pin | Prevents unauthorized direct access |
| Inheritance | All accounts inherit from BankAccount | Reuses common code (deposit, statements) |
| Method Overriding | Each account overrides withdraw() | Different accounts, different rules |
| Polymorphism | transfer_money() function | One function works with all account types |
| Class Methods | get_total_accounts() | Tracks bank-wide statistics |
| Static Methods | calculate_tds() | Utility functions, no object needed |
| Getter/Setter | get_balance(), set_pin() | Controlled access with validation |
Real-World Extension Ideas
- Customer Class: One person can have multiple accounts
- Loan System: Add
LoanAccountclass with EMI calculation - Credit Card: Add
CreditCardAccountwith billing cycles - Database Integration: Save accounts to SQLite/PostgreSQL
- Web Interface: Connect to Django for online banking
- Transaction Limits: Add daily/monthly spending limits
- Beneficiary Management: Save frequent transfer recipients
- 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.