🏗️ PHASE 3 · CAPSTONE PROJECTS

Phase 3
OOP Capstone

Classes, inheritance, file I/O, exceptions, and packages — all working together in 4 real, professional-grade programs. This is the finale.

4
PROJECTS
~4h
BUILD TIME
15
LESSONS COMPLETE
🏆
COURSE FINALE
LESSON 11
OOP & Classes
LESSON 12
Inheritance
LESSON 13
File Handling
LESSON 14
Exceptions
LESSON 15
Modules
🏦
PROJECT 01 OF 04
Banking System
A fully OOP bank with an Account class hierarchy, transaction history, JSON persistence, custom exceptions for overdraft and frozen accounts, and a robust interactive menu.
ABC + inheritance custom exceptions JSON persistence @property datetime
✅ Features
  • Abstract Account → SavingsAccount, CheckingAccount
  • SavingsAccount: interest rate, apply_interest()
  • CheckingAccount: overdraft limit + transaction fee
  • Full transaction log with datetime stamps
  • Custom exceptions: InsufficientFunds, AccountFrozen
  • Bank class: multi-account management + transfer + JSON save
🧠 Concepts
  • ABC + @abstractmethod for contract enforcement
  • super().__init__() chain for proper setup
  • @property with validation for balance
  • Custom exception hierarchy from BankError
  • json.dump/load for full state persistence
  • datetime.now().strftime() for transaction timestamps
banking_system.py
PYTHON
import json
from abc import ABC, abstractmethod
from datetime import datetime

# ── Custom Exceptions ─────────────────────────
class BankError(Exception): pass
class InsufficientFunds(BankError):
    def __init__(self, need, have):
        super().__init__(f"Need PKR {need:,.0f}, have PKR {have:,.0f}")
class AccountFrozen(BankError):
    def __init__(self): super().__init__("Account is frozen")

# ── Abstract Base Account ─────────────────────
class Account(ABC):
    def __init__(self, owner, acc_no, balance=0):
        self.owner    = owner
        self.acc_no   = acc_no
        self._balance = float(balance)
        self.frozen   = False
        self.history  = []

    @property
    def balance(self): return self._balance

    def _log(self, action, amount):
        self.history.append({"action": action, "amount": round(amount, 2),
            "balance": round(self._balance, 2),
            "time": datetime.now().strftime("%Y-%m-%d %H:%M")})

    def deposit(self, amount):
        if self.frozen: raise AccountFrozen()
        if amount <= 0: raise ValueError("Amount must be positive")
        self._balance += amount
        self._log("DEPOSIT", amount)

    @abstractmethod
    def withdraw(self, amount): pass

    @abstractmethod
    def account_type(self): pass

    def statement(self):
        print(f"\n  ── {self.account_type()} [{self.acc_no}] ──")
        print(f"  Owner  : {self.owner}")
        print(f"  Balance: PKR {self.balance:,.2f}")
        print(f"  {'Action':10} {'Amount':>12}  {'Balance':>12}  Time")
        print("  " + "─"*56)
        for t in self.history[-5:]:
            print(f"  {t['action']:10} PKR {t['amount']:>9,.0f}  PKR {t['balance']:>9,.0f}  {t['time']}")

    def to_dict(self):
        return {"type": self.account_type(), "owner": self.owner,
                "acc_no": self.acc_no, "balance": self._balance,
                "frozen": self.frozen, "history": self.history}


class SavingsAccount(Account):
    def __init__(self, owner, acc_no, balance=0, rate=0.06):
        super().__init__(owner, acc_no, balance)
        self.rate = rate
    def account_type(self): return "Savings"
    def withdraw(self, amount):
        if self.frozen: raise AccountFrozen()
        if amount > self._balance: raise InsufficientFunds(amount, self._balance)
        self._balance -= amount; self._log("WITHDRAW", amount)
    def apply_interest(self):
        i = self._balance * self.rate
        self._balance += i; self._log("INTEREST", i); return i


class CheckingAccount(Account):
    def __init__(self, owner, acc_no, balance=0, overdraft=10000):
        super().__init__(owner, acc_no, balance)
        self.overdraft = overdraft; self.fee = 50
    def account_type(self): return "Checking"
    def withdraw(self, amount):
        if self.frozen: raise AccountFrozen()
        total = amount + self.fee
        if total > self._balance + self.overdraft:
            raise InsufficientFunds(total, self._balance + self.overdraft)
        self._balance -= total
        self._log("WITHDRAW", amount); self._log("FEE", self.fee)


class Bank:
    def __init__(self, name): self.name = name; self.accounts = {}
    def add(self, acc): self.accounts[acc.acc_no] = acc
    def get(self, no):
        if no not in self.accounts: raise BankError(f"Account {no} not found")
        return self.accounts[no]
    def transfer(self, frm, to, amt):
        self.get(frm).withdraw(amt); self.get(to).deposit(amt)
    def list_accounts(self):
        print(f"\n  ── {self.name} Accounts ──")
        for a in self.accounts.values():
            print(f"  {a.acc_no}  {a.account_type():10} {a.owner:18} PKR {a.balance:>12,.2f}")
    def save(self, path="bank.json"):
        with open(path, "w") as f:
            json.dump([a.to_dict() for a in self.accounts.values()], f, indent=2)
        print("  ✅ Saved to bank.json")


if __name__ == "__main__":
    bank = Bank("BwB Bank")
    s1   = SavingsAccount("Ali Hassan",  "SA001", 50000)
    c1   = CheckingAccount("Sara Khan",   "CA001", 20000)
    bank.add(s1); bank.add(c1)
    s1.deposit(10000)
    s1.apply_interest()
    s1.withdraw(5000)
    bank.transfer("SA001", "CA001", 3000)
    try:
        s1.withdraw(999999)
    except InsufficientFunds as e:
        print(f"  ❌ {e}")
    s1.statement(); bank.list_accounts(); bank.save()
Extension ideas
Add a Bank.load(path) classmethod that reads bank.json and reconstructs SavingsAccount / CheckingAccount objects based on the "type" field. Add a FDAccount (Fixed Deposit) subclass with a maturity date and penalty for early withdrawal.
📦
PROJECT 02 OF 04
Product Inventory Manager
An OOP inventory with a Product hierarchy (Physical, Digital, Perishable), polymorphic sell(), CSV persistence, low-stock alerts, and a ranked revenue report.
polymorphism CSV persistence @property custom exceptions datetime.date
✅ Features
  • Product → PhysicalProduct, DigitalProduct, PerishableProduct
  • DigitalProduct: unlimited downloads, no stock deduction
  • PerishableProduct: expiry date, is_expired property
  • OutOfStockError, ExpiredProductError custom exceptions
  • Inventory: add/sell/report + low-stock alerts
  • CSV save and load preserving product type
🧠 Concepts
  • Polymorphism: Inventory calls sell() on any product type
  • @property: revenue, status, is_expired
  • Exception hierarchy: OutOfStockError(InventoryError)
  • csv.DictReader/DictWriter for persistence
  • sorted(key=lambda) for revenue ranking
  • date.fromisoformat() for expiry parsing
inventory_manager.py
PYTHON
import csv
from datetime import date

class InventoryError(Exception): pass
class OutOfStockError(InventoryError):
    def __init__(self, name, want, have):
        super().__init__(f"{name}: need {want}, only {have} in stock")
class ExpiredProductError(InventoryError):
    def __init__(self, name): super().__init__(f"{name} has expired")


class Product:
    def __init__(self, name, price, stock, category):
        self.name     = name
        self.price    = float(price)
        self.stock    = int(stock)
        self.category = category
        self.sold     = 0

    def sell(self, qty=1):
        if qty > self.stock:
            raise OutOfStockError(self.name, qty, self.stock)
        self.stock -= qty; self.sold += qty
        return self.price * qty

    @property
    def revenue(self): return self.price * self.sold

    @property
    def status(self):
        if self.stock == 0: return "Out of Stock 🔴"
        if self.stock <= 5:  return "Low Stock ⚠️"
        return "OK ✅"

    def __str__(self):
        return f"{self.name:22} PKR {self.price:>8,.0f}  Stock: {self.stock:4}  {self.status}"


class DigitalProduct(Product):
    """Downloads — stock never decreases."""
    def __init__(self, name, price):
        super().__init__(name, price, 999999, "Digital")
    def sell(self, qty=1):
        self.sold += qty; return self.price * qty

class PhysicalProduct(Product):
    def __init__(self, name, price, stock, weight_kg=1.0):
        super().__init__(name, price, stock, "Physical")
        self.weight_kg = weight_kg

class PerishableProduct(Product):
    def __init__(self, name, price, stock, expiry):
        super().__init__(name, price, stock, "Perishable")
        self.expiry = date.fromisoformat(expiry) if isinstance(expiry, str) else expiry
    @property
    def is_expired(self): return date.today() > self.expiry
    def sell(self, qty=1):
        if self.is_expired: raise ExpiredProductError(self.name)
        return super().sell(qty)


class Inventory:
    def __init__(self): self.products = {}
    def add(self, p): self.products[p.name] = p
    def sell(self, name, qty=1):
        if name not in self.products: raise InventoryError(f"'{name}' not in inventory")
        return self.products[name].sell(qty)
    def low_stock_alert(self):
        alerts = [p for p in self.products.values() if p.stock <= 5]
        if alerts: print("\n  ⚠️  LOW STOCK ALERTS:")
        for p in alerts: print(f"    {p.name}: {p.stock} remaining")
    def report(self):
        print(f"\n  {'Product':22} {'Cat':10} {'Price':>9}  {'Sold':5}  Revenue")
        print("  " + "─"*62)
        for p in sorted(self.products.values(), key=lambda x: -x.revenue):
            print(f"  {p.name:22} {p.category:10} PKR {p.price:>6,.0f}  {p.sold:5}  PKR {p.revenue:>9,.0f}")
        total = sum(p.revenue for p in self.products.values())
        print(f"\n  TOTAL REVENUE: PKR {total:,.0f}")
        self.low_stock_alert()

if __name__ == "__main__":
    inv = Inventory()
    inv.add(PhysicalProduct("Python Book",  1500, 50, 0.4))
    inv.add(DigitalProduct("Python Course", 2999))
    inv.add(PerishableProduct("Juice Pack", 250, 20, "2027-12-31"))
    inv.sell("Python Book", 12)
    inv.sell("Python Course", 35)
    inv.sell("Juice Pack", 18)   # only 2 left → low stock alert
    inv.report()
💡
Polymorphism is the whole point
The Inventory.sell() method calls p.sell(qty) on any product — Digital, Physical, or Perishable — each handling it differently without the Inventory knowing which type it is. This is the core OOP benefit: one interface, many behaviours.
⚔️
PROJECT 03 OF 04
Text-Based RPG
A turn-based RPG with a Character class hierarchy, combat system, inventory of items, level-up mechanics, and JSON save/load — structured as a proper multi-module package.
multi-class OOP inheritance JSON save/load random module __dunder__ methods
✅ Features
  • Character base → Warrior, Mage, Rogue subclasses
  • Each class has unique special_attack() method
  • Item class with consumable effects (heal, buff)
  • Turn-based combat loop: attack, special, heal, flee
  • Level-up system: XP → stat boosts
  • JSON save/load preserving full character state
🧠 Concepts
  • Polymorphism: special_attack() per subclass
  • @property for is_alive, hp_bar
  • __str__ for character status display
  • random.randint() for damage variance
  • Custom DeadCharacterError exception
  • @classmethod from_dict() alternative constructor
text_rpg.py
PYTHON
import random, json

class DeadCharacterError(Exception): pass

class Item:
    def __init__(self, name, effect, value):
        self.name  = name; self.effect = effect; self.value = value
    def use(self, char):
        if self.effect == "heal":
            char.hp = min(char.max_hp, char.hp + self.value)
            return f"  🧪 {char.name} used {self.name}: +{self.value} HP"
        elif self.effect == "buff":
            char.attack += self.value
            return f"  ⚡ {char.name} used {self.name}: +{self.value} ATK"


class Character:
    def __init__(self, name, hp, attack, defense):
        self.name    = name
        self.max_hp  = hp
        self.hp      = hp
        self.attack  = attack
        self.defense = defense
        self.xp      = 0
        self.level   = 1
        self.inventory= []

    @property
    def is_alive(self): return self.hp > 0

    @property
    def hp_bar(self):
        filled = int(self.hp / self.max_hp * 20)
        return f"[{'█'*filled}{'░'*(20-filled)}] {self.hp}/{self.max_hp}"

    def take_damage(self, dmg):
        actual = max(1, dmg - self.defense)
        self.hp  = max(0, self.hp - actual); return actual

    def basic_attack(self, target):
        if not self.is_alive: raise DeadCharacterError(f"{self.name} is dead")
        dmg = random.randint(int(self.attack*0.8), self.attack)
        actual = target.take_damage(dmg)
        return f"  ⚔️  {self.name} attacks {target.name} for {actual} damage"

    def special_attack(self, target):
        return self.basic_attack(target)   # overridden in subclasses

    def gain_xp(self, amount):
        self.xp += amount
        if self.xp >= self.level * 100:
            self.level += 1; self.max_hp += 20; self.hp = self.max_hp
            self.attack += 5; self.defense += 2
            return f"  🌟 LEVEL UP! {self.name} is now level {self.level}!"
        return f"  +{amount} XP ({self.xp}/{self.level*100})"

    def __str__(self):
        return f"  {self.name} [Lv{self.level} {type(self).__name__}]\n  HP {self.hp_bar}\n  ATK {self.attack}  DEF {self.defense}"

    def to_dict(self):
        return {"class": type(self).__name__, "name": self.name,
                "max_hp": self.max_hp, "hp": self.hp, "attack": self.attack,
                "defense": self.defense, "xp": self.xp, "level": self.level}

    @classmethod
    def from_dict(cls, d):
        klass = {"Warrior":Warrior, "Mage":Mage, "Rogue":Rogue}[d["class"]]
        obj = klass(d["name"]); obj.__dict__.update(d); return obj


class Warrior(Character):
    def __init__(self, name):
        super().__init__(name, hp=120, attack=20, defense=10)
    def special_attack(self, target):
        dmg = max(1, self.attack * 2 - target.defense)
        target.hp = max(0, target.hp - dmg)
        return f"  🗡️  POWER STRIKE! {self.name} deals {dmg} to {target.name}!"

class Mage(Character):
    def __init__(self, name):
        super().__init__(name, hp=80, attack=30, defense=3)
        self.mana = 50
    def special_attack(self, target):
        if self.mana < 20: return f"  💨 Not enough mana!"
        self.mana -= 20
        dmg = self.attack + random.randint(10, 25)
        target.hp = max(0, target.hp - dmg)
        return f"  🔥 FIREBALL! {self.name} blasts {target.name} for {dmg}!"

class Rogue(Character):
    def __init__(self, name):
        super().__init__(name, hp=90, attack=25, defense=5)
    def special_attack(self, target):
        if random.random() < 0.4:   # 40% crit chance
            dmg = self.attack * 3; target.hp = max(0, target.hp - dmg)
            return f"  ⚡ CRITICAL BACKSTAB! {self.name} hits {target.name} for {dmg}!"
        return self.basic_attack(target)


def combat(hero, enemy):
    print(f"\n  ⚔️  {hero.name} vs {enemy.name}!\n")
    while hero.is_alive and enemy.is_alive:
        print(hero); print(enemy)
        print("\n  1) Attack  2) Special  3) Quit")
        ch = input("  Action: ").strip()
        if   ch == "1": print(hero.basic_attack(enemy))
        elif ch == "2": print(hero.special_attack(enemy))
        else: print("  🏃 Fled!"); break
        if enemy.is_alive: print(enemy.basic_attack(hero))
    if not enemy.is_alive:
        print(f"\n  🏆 {hero.name} wins!")
        print(hero.gain_xp(60))
    elif not hero.is_alive:
        print(f"\n  💀 {hero.name} was defeated...")

if __name__ == "__main__":
    print("  Choose: 1) Warrior  2) Mage  3) Rogue")
    c    = input("  Class : ")
    name = input("  Name  : ")
    hero = {"1":Warrior, "2":Mage, "3":Rogue}.get(c, Warrior)(name)
    goblin = Warrior("Goblin")
    goblin.hp = 50; goblin.max_hp = 50; goblin.attack = 12
    combat(hero, goblin)
    with open("save.json", "w") as f: json.dump(hero.to_dict(), f, indent=2)
    print("  💾 Character saved!")
Extension ideas
Add an Enemy factory function that creates random enemies by reading from a JSON monster list. Add a Shop class that sells Items, persisted to a separate JSON. Add a WorldMap class that tracks explored rooms and triggers different enemy encounters.
📊
PROJECT 04 OF 04
Student Analytics Platform
A complete academic management system: Student + Course classes, CSV/JSON dual persistence, GPA calculation with credit weighting, transcript generation, and a module package structure — tying every Phase 3 concept together.
multi-class OOP CSV + JSON I/O package structure exceptions @classmethod
✅ Features
  • Student class: ID, name, email, courses dict, grades dict
  • Course class: code, name, credits, max_capacity, enrollment counter
  • School class: manages all students and courses
  • @property gpa with weighted credit formula
  • Transcript generator with letter grades
  • Dual persistence: CSV for students, JSON for full school data
🧠 Concepts
  • Many-to-many: Student ↔ Course via dicts
  • @classmethod School.from_json() constructor
  • Custom EnrollmentError, GradeError exceptions
  • __len__, __contains__ on School
  • csv.DictReader for student import
  • pathlib.Path for cross-platform file paths
student_platform.py
PYTHON
import csv, json
from pathlib import Path

class AcademicError(Exception): pass
class EnrollmentError(AcademicError): pass
class GradeError(AcademicError): pass


class Course:
    def __init__(self, code, name, credits, capacity=30):
        self.code     = code
        self.name     = name
        self.credits  = int(credits)
        self.capacity = int(capacity)
        self.enrolled = 0
    def enroll(self):
        if self.enrolled >= self.capacity:
            raise EnrollmentError(f"{self.code} full ({self.capacity}/{self.capacity})")
        self.enrolled += 1
    def to_dict(self): return {"code":self.code, "name":self.name, "credits":self.credits, "capacity":self.capacity}
    def __str__(self): return f"[{self.code}] {self.name} ({self.credits} cr)"


class Student:
    def __init__(self, sid, name, email):
        self.sid     = sid
        self.name    = name
        self.email   = email
        self.courses = {}   # {code: Course}
        self.grades  = {}   # {code: mark 0-100}

    def enroll(self, course):
        if course.code in self.courses:
            raise EnrollmentError(f"Already enrolled in {course.code}")
        course.enroll(); self.courses[course.code] = course

    def add_grade(self, code, mark):
        if code not in self.courses: raise GradeError(f"Not enrolled in {code}")
        if not (0 <= mark <= 100): raise GradeError("Mark must be 0–100")
        self.grades[code] = mark

    @property
    def gpa(self):
        if not self.grades: return 0.0
        pts = cr = 0
        for code, mark in self.grades.items():
            c = self.courses[code].credits
            pts += (mark/100*4.0) * c; cr += c
        return round(pts/cr, 2) if cr else 0.0

    def transcript(self):
        print(f"\n  ═══ TRANSCRIPT: {self.name} [{self.sid}] ═══")
        print(f"  {'Code':8} {'Course':28} {'Cr':>3}  {'Mark':>5}  Grade")
        print("  " + "─"*55)
        for code, c in sorted(self.courses.items()):
            m = self.grades.get(code, "—")
            g = ("A+" if m>=95 else "A" if m>=85 else "B" if m>=70 else "C" if m>=55 else "F") if isinstance(m,int) else "—"
            print(f"  {code:8} {c.name:28} {c.credits:>3}  {str(m):>5}  {g}")
        print(f"\n  Cumulative GPA: {self.gpa:.2f} / 4.00")
        honour = "  🏆 Honour Roll!" if self.gpa >= 3.7 else ""
        if honour: print(honour)

    def to_dict(self):
        return {"sid":self.sid, "name":self.name, "email":self.email,
                "courses":list(self.courses.keys()), "grades":self.grades}


class School:
    def __init__(self, name):
        self.name     = name
        self.students = {}
        self.courses  = {}

    def add_student(self, s): self.students[s.sid] = s
    def add_course(self, c):  self.courses[c.code] = c
    def __len__(self): return len(self.students)
    def __contains__(self, sid): return sid in self.students

    def enroll(self, sid, code):
        self.students[sid].enroll(self.courses[code])

    def grade(self, sid, code, mark):
        self.students[sid].add_grade(code, mark)

    def honour_roll(self):
        return sorted([s for s in self.students.values() if s.gpa >= 3.7],
                       key=lambda x: -x.gpa)

    def save(self, path="school.json"):
        with open(path, "w") as f:
            json.dump({"name":self.name,
                       "courses":[c.to_dict() for c in self.courses.values()],
                       "students":[s.to_dict() for s in self.students.values()]},
                      f, indent=2)
        print(f"  ✅ Saved {len(self)} students to {path}")


if __name__ == "__main__":
    school = School("BitWithBite Academy")

    # Add courses
    for cdata in [("PY101","Python Fundamentals",3),
                   ("WD201","Web Development",4),
                   ("DS301","Data Science",4)]:
        school.add_course(Course(*cdata))

    # Add students
    students = [("S001","Ali Hassan","ali@bwb.com"),
                 ("S002","Sara Khan","sara@bwb.com"),
                 ("S003","Fatima Sheikh","fatima@bwb.com")]
    for sdata in students:
        school.add_student(Student(*sdata))

    # Enroll and grade
    for sid in ["S001","S002","S003"]:
        school.enroll(sid, "PY101"); school.enroll(sid, "WD201")
    school.enroll("S001", "DS301")

    school.grade("S001", "PY101", 92); school.grade("S001", "WD201", 88)
    school.grade("S001", "DS301", 95)
    school.grade("S002", "PY101", 78); school.grade("S002", "WD201", 82)
    school.grade("S003", "PY101", 96); school.grade("S003", "WD201", 91)

    # Transcripts
    for s in school.students.values(): s.transcript()

    # Honour roll
    print("\n  🏆 HONOUR ROLL:")
    for s in school.honour_roll():
        print(f"    {s.name:20} GPA {s.gpa:.2f}")

    school.save()
💡
This is a real-world architecture
The Student + Course + School pattern is exactly how university management systems, LMS platforms, and student portals are built at a basic level — objects with clear responsibilities, relationships via dicts/lists, persistence via JSON/CSV, and business logic (GPA, honour roll, capacity) encapsulated inside the relevant class.
🏆
Python Mastery Complete!

You've gone from print("Hello") to building OOP systems with inheritance, file I/O, custom exceptions, and packages. You are now a Python developer. What you build next is up to you.

Variables & Types ✓ Strings ✓ Operators ✓ Input/Output ✓ Conditionals ✓ Loops ✓ Lists & Tuples ✓ Dicts & Sets ✓ Functions ✓ *args / **kwargs ✓ Lambda ✓ LEGB Scope ✓ OOP & Classes ✓ Inheritance ✓ Polymorphism ✓ Abstract Classes ✓ File Handling ✓ CSV + JSON ✓ Exception Handling ✓ Custom Exceptions ✓ Context Managers ✓ Modules & Packages ✓ pip & venv ✓ Standard Library ✓
🚀 What to learn next
  • 🌐Web Development — Build full web apps with Flask or Django. Your Python knowledge transfers directly.
  • 📊Data Science — Learn pandas, NumPy, matplotlib. Python is the #1 language in data science.
  • 🤖Machine Learning — Scikit-learn, TensorFlow, PyTorch. Build models that learn from data.
  • APIs & Automation — Automate tasks, scrape websites, call REST APIs with requests.
  • 🏗️Build a real project — The best way to solidify skills is to build something you care about and ship it.
Start a New Course →
Course Complete 🎉15 Lessons · 3 Phases