Classes, inheritance, file I/O, exceptions, and packages — all working together in 4 real, professional-grade programs. This is the finale.
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()
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.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()
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.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!")
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()
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.