What I Learned in Week 5 of Python — OOP Deep Dive
Week 4 ended with me putting functions inside a class for the first time.
Week 5 started with me realising that was barely scratching the surface.
OOP isn't a feature of Python — it's a completely different way of thinking about programs. Instead of asking "what does this code do?" you start asking "what things exist in this system, and what can they do?" One week later I've built a bank account system, two subclass hierarchies, a library with composition, and a full task management system using OOP throughout.
Here's how it went.
Day 29 — Classes & Objects: Building a Bank Account
The first real class I built from scratch. A BankAccount that holds state, tracks history, and enforces rules — all inside one object.
class BankAccount:
def __init__(self, name, balance):
self.name = name
self.balance = balance
self.history = [f"Account created with account name: {self.name} and account balance: {self.balance}"]
def __str__(self):
return f"BankAccount({self.name}, {self.balance})"
def deposit(self, deposit_amount):
self.balance += deposit_amount
self.history.append(
f"Amount {deposit_amount} deposited in {self.name} account. Total Balance = ${self.balance}"
)
return self.balance
def withdraw(self, withdrawal_amount):
if self.balance >= withdrawal_amount:
self.balance -= withdrawal_amount
self.history.append(
f"Amount {withdrawal_amount} withdrawn from {self.name} account. Total Balance = ${self.balance}"
)
return self.balance
else:
self.history.append(f"Failed to withdraw ${withdrawal_amount}; insufficient funds")
return self.balance
def transfer(self, amount, recipient):
if isinstance(recipient, BankAccount):
if self.balance >= amount:
self.balance -= amount
recipient.balance += amount
self.history.append(f"Transferred ${amount} to {recipient.name}")
recipient.history.append(f"Received ${amount} from {self.name}")
return self.balance
else:
self.history.append("Insufficient funds, failed to transfer")
return self.balance
else:
print("Not a valid bank account!")
return self.balance
def get_balance(self):
return self.balance
def get_history(self):
return self.history
# Getter and setter for balance
@property
def balance(self):
return self._balance
@balance.setter
def balance(self, balance):
try:
self._balance = float(balance)
except ValueError:
print("Enter numbers")
self._balance = 0.0
# Getter and setter for name
@property
def name(self):
return self._name
@name.setter
def name(self, name):
if not name:
print("Missing name!")
self._name = "Unnamed"
else:
self._name = name
def main():
# Create accounts
alice = BankAccount("Alice", 100)
bob = BankAccount("Bob", 50)
# Test basic operations
alice.deposit(25)
alice.withdraw(10)
alice.withdraw(200) # Should fail
# Test transfer
alice.transfer(30, bob)
# Inspect results
print("Alice balance:", alice.get_balance()) # Expected: 85
print("Bob balance:", bob.get_balance()) # Expected: 80
print("\nAlice history:")
for line in alice.get_history():
print(line)
print("\nBob history:")
for line in bob.get_history():
print(line)
if __name__ == "__main__":
main()
Three things that landed hard on Day 29:
-
__init__is not a constructor in the traditional sense — it's an initialiser. The object already exists when__init__runs. It just sets the starting state. -
selfis just the object referring to itself. Once that clicked, method signatures stopped looking strange. -
isinstance(recipient, BankAccount)intransfer()— type-checking before operating on another object. That's the class enforcing its own rules, not the caller's responsibility.
The history list on each account was my favourite part of this design. Every operation appends a human-readable log entry. By the end of a session, calling get_history() shows the complete life of that account. That feels genuinely useful.
Day 30 — Inheritance: CheckingAccount and SavingsAccount
If Day 29 was "what is a class," Day 30 was "what happens when classes are related."
CheckingAccount and SavingsAccount both inherit from BankAccount — they get everything the parent has, and add their own rules on top. CheckingAccount allows overdrafts (with a $35 fee). SavingsAccount earns interest.
from bank_account import BankAccount
class CheckingAccount(BankAccount):
def __init__(self, name, balance, overdraft_limit = 16):
super().__init__(name, balance)
self.overdraft_limit = overdraft_limit
def withdraw(self, amount):
if self.balance >= amount:
self.balance -= amount
self.history.append(f"Amount {amount} withdrawn from {self.name} account. Total Balance = ${self.balance}")
return self.balance
else:
if self.balance - amount -35 < -self.overdraft_limit:
self.history.append(f"Withdrawal rejected! exceeded the overdraft limit")
return self.balance
self.balance -= (amount + 35)
self.history.append(f"Overdraft: Withdrew ${amount} with $35 fee. New balance: ${self.balance}")
return self.balance
def transfer(self, amount, recipient):
if isinstance(recipient, BankAccount):
if self.balance >= amount:
self.balance -= amount
recipient.balance += amount
self.history.append(f"Transferred ${amount} to {recipient.name}")
recipient.history.append(f"Received ${amount} from {self.name}")
return self.balance
self.balance -= (amount + 35)
self.history.append(f"Overdraft: Tansferred ${amount} with $35 fee to {recipient.name}. New balance: ${self.balance}")
recipient.balance += amount
self.history.append(f"Received ${amount} from {self.name}")
return self.balance
else:
print("Not a valid bank account!")
return self.balance
class SavingsAccount(BankAccount):
def __init__(self, name, balance, intrest_rate = 0.02):
super().__init__(name, balance)
self.intrest_rate = intrest_rate
def apply_intrest(self):
intrest = self.balance * self.intrest_rate
self.balance += intrest
self.history.append(f"Interest applied: ${intrest}. New balance: ${self.balance}")
return self.balance
def main():
# --- Checking Account Tests ---
print("=== Checking Account ===")
checking = CheckingAccount("Alice", 100, overdraft_limit=100)
checking.withdraw(50) # Normal: balance 50
checking.withdraw(80) # Overdraft: balance -65 (80 + 35 fee)
checking.withdraw(100) # Exceeds overdraft limit: should fail, balance stays -65
checking.deposit(20) # Balance -45
print("Checking balance:", checking.get_balance()) # Expected: -45
print("Checking history:")
for line in checking.get_history():
print(line)
# --- Savings Account Tests ---
print("\n=== Savings Account ===")
savings = SavingsAccount("Bob", 1000, intrest_rate=0.05)
savings.apply_intrest() # Balance 1050
savings.withdraw(2000) # Should fail (no overdraft)
savings.withdraw(50) # Balance 1000
print("Savings balance:", savings.get_balance()) # Expected: 1000
print("Savings history:")
for line in savings.get_history():
print(line)
# --- Cross-account Transfer ---
print("\n=== Cross-Account Transfer ===")
checking2 = CheckingAccount("Charlie", 50, overdraft_limit=100)
savings2 = SavingsAccount("Dana", 200, intrest_rate=0.02)
checking2.transfer(75, savings2) # Charlie overdrafts by 25 + 35 fee. Dana receives 75.
print("Charlie balance:", checking2.get_balance()) # Expected: -60
print("Dana balance:", savings2.get_balance()) # Expected: 275
if __name__ == "__main__":
main()
Two things that clicked:
-
super().__init__(name, balance)— calling the parent class's__init__from the child. Without this, the inherited attributes and history list wouldn't exist.super()is the clean way to say "do everything the parent does, then do my extra stuff." -
Method overriding —
CheckingAccountdefines its ownwithdraw()that replaces the parent's version. It doesn't break any existing code — objects that wereBankAccountinstances still work. Subclasses can specialise without touching the parent.
The cross-account transfer test was the most satisfying: Charlie (a CheckingAccount) transfers more than his balance to Dana (a SavingsAccount). The overdraft fires, adds the fee, and both histories log the event accurately. Two different account types, one shared transaction method.
Day 31 — Encapsulation & Properties
Encapsulation is the idea that an object's internal data should be accessed through the object, not grabbed directly. Python doesn't enforce this by convention the way some languages do — but @property gives you the tools to control it.
The @property decorators were already in bank_account.py from Day 29. Day 31 was understanding why they're there:
# Getter and setter for balance
@property
def balance(self):
return self._balance
@balance.setter
def balance(self, balance):
try:
self._balance = float(balance)
except ValueError:
print("Enter numbers")
self._balance = 0.0
# Getter and setter for name
@property
def name(self):
return self._name
@name.setter
def name(self, name):
if not name:
print("Missing name!")
self._name = "Unnamed"
else:
self._name = name
The _balance and _name attributes are private by convention — the underscore is a signal to other developers not to access them directly. The @property getter lets code read alice.balance as normal. The @balance.setter intercepts any attempt to write alice.balance = value and validates it first.
The result: you can't accidentally set a balance to a string. The setter catches it with try/except ValueError, resets to 0.0, and the account keeps running. The object protects its own data.
Day 32 — Dunder Methods: Making Objects Feel Native
Dunder (double-underscore) methods are how you teach Python's built-in operators what to do with your custom objects. __str__, __repr__, __eq__ — these make your class feel like a native Python type.
class BankAccount:
def __init__(self, name, balance):
self.name = name
self.balance = balance
self.history = [f"Account created with account name: {self.name} and account balance: {self.balance}"]
def __str__(self):
return f"BankAccount({self.name}, {self.balance})"
def __repr__(self):
return f"BankAccount: (name = {self.name}, balance = {self.balance})"
def __eq__(self, other):
if not isinstance(other, BankAccount):
print("NotImplemented!")
return self.name == other.name and self.balance == other.balance
def deposit(self, deposit_amount):
self.balance += deposit_amount
self.history.append(
f"Amount {deposit_amount} deposited in {self.name} account. Total Balance = ${self.balance}"
)
return self.balance
def withdraw(self, withdrawal_amount):
if self.balance >= withdrawal_amount:
self.balance -= withdrawal_amount
self.history.append(
f"Amount {withdrawal_amount} withdrawn from {self.name} account. Total Balance = ${self.balance}"
)
return self.balance
else:
self.history.append(f"Failed to withdraw ${withdrawal_amount}; insufficient funds")
return self.balance
def transfer(self, amount, recipient):
if isinstance(recipient, BankAccount):
if self.balance >= amount:
self.balance -= amount
recipient.balance += amount
self.history.append(f"Transferred ${amount} to {recipient.name}")
recipient.history.append(f"Received ${amount} from {self.name}")
return self.balance
else:
self.history.append("Insufficient funds, failed to transfer")
return self.balance
else:
print("Not a valid bank account!")
return self.balance
def get_balance(self):
return self.balance
def get_history(self):
return self.history
# Getter and setter for balance
@property
def balance(self):
return self._balance
@balance.setter
def balance(self, balance):
try:
self._balance = float(balance)
except ValueError:
print("Enter numbers")
self._balance = 0.0
# Getter and setter for name
@property
def name(self):
return self._name
@name.setter
def name(self, name):
if not name:
print("Missing name!")
self._name = "Unnamed"
else:
self._name = name
def main():
# Create accounts
alice = BankAccount("Alice", 100)
bob = BankAccount("Bob", 50)
# Test basic operations
alice.deposit(25)
alice.withdraw(10)
alice.withdraw(200) # Should fail
# Test transfer
alice.transfer(30, bob)
# Inspect results
print("Alice balance:", alice.get_balance()) # Expected: 85
print("Bob balance:", bob.get_balance()) # Expected: 80
print("\nAlice history:")
for line in alice.get_history():
print(line)
print("\nBob history:")
for line in bob.get_history():
print(line)
if __name__ == "__main__":
main()
And the subclasses got their own __str__ and __repr__ too:
class CheckingAccount(BankAccount):
def __init__(self, name, balance, overdraft_limit = 16):
super().__init__(name, balance)
self.overdraft_limit = overdraft_limit
def __str__(self):
return f"This is the Checking Account, the name of the owner is {self.name} and the balance is {self.balance}."
def __repr__(self):
return f"Checking Account: (name = {self.name}, balance = {self.balance}, overdraft_limit = {self.overdraft_limit})"
def withdraw(self, amount):
if self.balance >= amount:
self.balance -= amount
self.history.append(f"Amount {amount} withdrawn from {self.name} account. Total Balance = ${self.balance}")
return self.balance
else:
if self.balance - amount -35 < -self.overdraft_limit:
self.history.append(f"Withdrawal rejected! exceeded the overdraft limit")
return self.balance
self.balance -= (amount + 35)
self.history.append(f"Overdraft: Withdrew ${amount} with $35 fee. New balance: ${self.balance}")
return self.balance
def transfer(self, amount, recipient):
if isinstance(recipient, BankAccount):
if self.balance >= amount:
self.balance -= amount
recipient.balance += amount
self.history.append(f"Transferred ${amount} to {recipient.name}")
recipient.history.append(f"Received ${amount} from {self.name}")
return self.balance
self.balance -= (amount + 35)
self.history.append(f"Overdraft: Tansferred ${amount} with $35 fee to {recipient.name}. New balance: ${self.balance}")
recipient.balance += amount
self.history.append(f"Received ${amount} from {self.name}")
return self.balance
else:
print("Not a valid bank account!")
return self.balance
class SavingsAccount(BankAccount):
def __init__(self, name, balance, intrest_rate = 0.02):
super().__init__(name, balance)
self.intrest_rate = intrest_rate
def __str__(self):
return f"This is the Savings Account, the name of the owner is {self.name} and the balance is {self.balance}."
def __repr__(self):
return f"Savings Account: (name = {self.name}, balance = {self.balance}, intrest_rate = {self.intrest_rate})"
def apply_intrest(self):
intrest = self.balance * self.intrest_rate
self.balance += intrest
self.history.append(f"Interest applied: ${intrest}. New balance: ${self.balance}")
return self.balance
def main():
# --- Checking Account Tests ---
print("=== Checking Account ===")
checking = CheckingAccount("Alice", 100, overdraft_limit=100)
checking.withdraw(50) # Normal: balance 50
checking.withdraw(80) # Overdraft: balance -65 (80 + 35 fee)
checking.withdraw(100) # Exceeds overdraft limit: should fail, balance stays -65
checking.deposit(20) # Balance -45
print("Checking balance:", checking.get_balance()) # Expected: -45
print("Checking history:")
for line in checking.get_history():
print(line)
# --- Savings Account Tests ---
print("\n=== Savings Account ===")
savings = SavingsAccount("Bob", 1000, intrest_rate=0.05)
savings.apply_intrest() # Balance 1050
savings.withdraw(2000) # Should fail (no overdraft)
savings.withdraw(50) # Balance 1000
print("Savings balance:", savings.get_balance()) # Expected: 1000
print("Savings history:")
for line in savings.get_history():
print(line)
# --- Cross-account Transfer ---
print("\n=== Cross-Account Transfer ===")
checking2 = CheckingAccount("Charlie", 50, overdraft_limit=100)
savings2 = SavingsAccount("Dana", 200, intrest_rate=0.02)
checking2.transfer(75, savings2) # Charlie overdrafts by 25 + 35 fee. Dana receives 75.
print("Charlie balance:", checking2.get_balance()) # Expected: -60
print("Dana balance:", savings2.get_balance()) # Expected: 275
if __name__ == "__main__":
main()
The distinction between __str__ and __repr__ took a while to settle:
-
__str__is for humans —print(account)should give readable output -
__repr__is for developers —repr(account)should give enough detail to reconstruct the object -
__eq__letsaccount1 == account2mean something real: same name, same balance
Before dunders, printing an object gave something like <__main__.BankAccount object at 0x7f...>. After dunders, it gives BankAccount(Alice, 85.0). The object speaks for itself.
Day 33 — Composition vs Inheritance: A Library System
This was the conceptual centrepiece of the week. The question: when should classes inherit, and when should they contain each other?
The rule I took away: "is-a" → inheritance. "has-a" → composition.
A CheckingAccount is a BankAccount. Inheritance makes sense.
A Library has Books and Members. Composition makes sense. A library is not a book — it shouldn't inherit from one.
from datetime import datetime, timedelta
class Book:
def __init__(self, title, author, isbn, is_available=True):
self.title = title
self.author = author
self.isbn = isbn
self.is_available = is_available
def __str__(self):
return f"{self.title} by {self.author} (ISBN: {self.isbn})"
def mark_borrowed(self):
self.is_available = False
def mark_returned(self):
self.is_available = True
class Member:
def __init__(self, name, member_id):
self.name = name
self.member_id = member_id
self.loans = []
def __str__(self):
return f"{self.name} (ID: {self.member_id})"
def borrow_book(self, book, library):
"""Ask library to loan a book to this member."""
if not isinstance(book, Book):
print("Invalid book.")
return False
if not book.is_available:
print("Book is currently not available")
return False
# Create the Loan (composition: Loan HAS Book and Member)
due = (datetime.now() + timedelta(days=14)).strftime("%Y-%m-%d")
loan = Loan(book, self, due)
# Track it everywhere
self.loans.append(loan)
library.record_loan(loan)
return True
def return_book(self, loan, library):
"""Return a specific loan via the library."""
if loan in self.loans:
self.loans.remove(loan)
library.close_loan(loan)
else:
print("Loan not found for this member.")
class Loan:
def __init__(self, book, member, due_date):
self.book = book # Loan HAS a Book
self.member = member # Loan HAS a Member
self.due_date = due_date
def __str__(self):
return f"Loan: {self.book.title} to {self.member.name}, due {self.due_date}"
def __repr__(self):
return f"Loan(book={self.book!r}, member={self.member!r}, due_date={self.due_date!r})"
class Library:
def __init__(self, name):
self.name = name
self.books = [] # Library HAS Books
self.members = [] # Library HAS Members
self.loans = [] # Library HAS Loans
def __str__(self):
return f"{self.name}: {len(self.books)} books, {len(self.members)} members"
def add_book(self, book):
if isinstance(book, Book):
self.books.append(book)
def register_member(self, member):
if isinstance(member, Member):
self.members.append(member)
def record_loan(self, loan):
"""Called by Member.borrow_book to register a loan."""
if isinstance(loan, Loan):
loan.book.mark_borrowed()
self.loans.append(loan)
def close_loan(self, loan):
"""Called by Member.return_book to close a loan."""
if loan in self.loans:
loan.book.mark_returned()
self.loans.remove(loan)
def get_available_books(self):
return [book for book in self.books if book.is_available]
if __name__ == "__main__":
lib = Library("CS50 Library")
b1 = Book("The C Programming Language", "Kernighan & Ritchie", "9780131103627")
b2 = Book("Clean Code", "Robert C. Martin", "9780132350884")
lib.add_book(b1)
lib.add_book(b2)
alice = Member("Alice", "M001")
lib.register_member(alice)
print("Available before borrow:", [b.title for b in lib.get_available_books()])
alice.borrow_book(b1, lib)
print("Available after borrow:", [b.title for b in lib.get_available_books()])
print("Alice's loans:", alice.loans)
alice.return_book(alice.loans[0], lib)
print("Available after return:", [b.title for b in lib.get_available_books()])
Four classes. Each one focused. Each one responsible for exactly its own data:
-
Bookknows if it's available -
Memberknows their loans -
Loanknows its book, member, and due date -
Librarycoordinates everything without owning any individual item's logic
The Loan class is the key insight here. It's the relationship between a Book and a Member, captured as its own object. When a loan is created inside borrow_book(), it's automatically due in 14 days via timedelta(days=14). When it's returned, close_loan() flips the book's availability back. Clean handoffs, no tight coupling.
That list comprehension in get_available_books() — [book for book in self.books if book.is_available] — is Week 3 knowledge powering Week 5 design.
Day 34 — The Week 5 Project: A Task Management System
The final project pulled the whole week together: a task management system with User, Project, and Task objects, property validation using regex, and proper OOP throughout.
from datetime import date
import re
#Build a task management system with users, projects, tasks, deadlines
class User:
def __init__(self, name):
self.name = name
self.projects = {}
def __str__(self):
return f"The User name is {self.name}"
def __repr__(self):
return f"User: (name = {self.name})"
def create_project(self, title, deadline):
project = Project(title, deadline)
self.projects[project.title] = project
return project
def mark_complete(self, project_title):
project = self.projects[project_title]
project.completed = True
def remove_project(self, project_title):
del self.projects[project_title]
def show_projects(self):
for project in self.projects.values():
status = "overdue" if project.is_overdue else "ok"
print(f"Project title: {project.title} | Deadline: {project.deadline} | Status: {status}")
@property
def name(self):
return self._name
@name.setter
def name(self, name):
if match := re.search(r"^(\w+ ?)$", name, re.IGNORECASE):
self._name = match.group(1)
else:
raise ValueError("Enter Words!")
class Project:
def __init__(self, title, deadline, completed = False):
self.title = title
self.deadline = deadline
self.completed = completed
self.tasks = []
def __str__(self):
return f"The Project Title is {self.title} and the deadline is {self.deadline}"
def __repr__(self):
return f"Project: (title = {self.title}, deadline = {self.deadline}, completed = {self.completed})"
def add_task(self, task):
self.tasks.append(task)
def remove_task(self, task):
self.tasks.remove(task)
def view_tasks(self):
for task in self.tasks:
print(task)
def is_overdue(self):
return not self.completed and self.deadline < date.today()
@property
def deadline(self):
return self._deadline
@deadline.setter
def deadline(self, deadline):
if match := re.search(r"^(\d{2})/ ?(\d{2})/ ?(\d{4})$", deadline):
try:
self._deadline = date(int(match.group(3)), int(match.group(2)), int(match.group(1)))
except ValueError:
print("Day should be from 1 - 31, Month must be from 1-12")
else:
raise ValueError("Enter the deadline in dd/mm/yyyy")
def main():
# --- Test User creation and string representation ---
print("=" * 50)
print("TEST 1: User Creation")
print("=" * 50)
alice = User("Alice")
print(f"__str__: {alice}")
print(f"__repr__: {repr(alice)}")
print()
# --- Test Project creation via User ---
print("=" * 50)
print("TEST 2: Project Creation")
print("=" * 50)
website = alice.create_project("Website", "05/06/2026")
mobile_app = alice.create_project("MobileApp", "01/01/2025")
print(f"__str__: {website}")
print(f"__repr__: {repr(website)}")
print()
# --- Test Task management ---
print("=" * 50)
print("TEST 3: Task Management")
print("=" * 50)
website.add_task("Design homepage")
website.add_task("Setup database")
website.add_task("Write API")
print("Tasks in 'Website':")
website.view_tasks()
website.remove_task("Setup database")
print("\nAfter removing 'Setup database':")
website.view_tasks()
print()
# --- Test Overdue Check ---
print("=" * 50)
print("TEST 4: Overdue Check")
print("=" * 50)
print(f"Website overdue? {website.is_overdue()}")
print(f"MobileApp overdue? {mobile_app.is_overdue()}")
print()
# --- Test Show Projects ---
print("=" * 50)
print("TEST 5: Show All Projects")
print("=" * 50)
alice.show_projects()
print()
# --- Test Mark Complete ---
print("=" * 50)
print("TEST 6: Mark Complete")
print("=" * 50)
alice.mark_complete("Website")
print(f"After marking complete: {repr(website)}")
print(f"Website overdue? {website.is_overdue()}")
print()
# --- Test Remove Project ---
print("=" * 50)
print("TEST 7: Remove Project")
print("=" * 50)
alice.remove_project("MobileApp")
alice.show_projects()
print()
# --- Test Name Validation ---
print("=" * 50)
print("TEST 8: Validation")
print("=" * 50)
try:
bad_user = User("Alice123!!!")
except ValueError as e:
print(f"Caught expected error: {e}")
try:
bad_project = Project("Bad", "not-a-date")
except ValueError as e:
print(f"Caught expected error: {e}")
if __name__ == "__main__":
main()
This is the most mature code I've written so far. A few things that stand out:
-
Walrus operator in setters —
if match := re.search(...)assigns and tests in one expression. The deadline setter validates and converts a"dd/mm/yyyy"string into a realdateobject using regex capture groups, raising a properValueErrorif it fails. -
is_overdue()usesdate.today()— comparing a stored deadline against today's date to compute real status dynamically. -
show_projects()callsis_overdue()on each project and reports "overdue" or "ok" without needing any stored status field — the answer is computed live from the data. -
Eight structured tests in
main()— each labelled, each verifying a specific feature. I started writing tests like this during the week and it immediately made debugging faster.
The walrus operator (:=) was something I found while writing the regex validation and it immediately became my favourite small Python feature. Assign and check in one line. Clean.
📁 What I Built This Week
| Project | File | Concepts Used |
|---|---|---|
| Bank Account | bank_account.py |
__init__, __str__, @property, deposit/withdraw/transfer/history |
| Account Types | account_types.py |
Inheritance, super(), method overriding, overdraft, interest |
| Dunder Methods |
bank_objects.py + account_objects.py
|
__str__, __repr__, __eq__, subclass dunders |
| Library System | library.py |
Composition, 4 classes, timedelta, list comprehension |
| Task Manager | tasks.py |
OOP throughout, walrus operator, regex in setters, date.today(), 8 test cases |
All the code is on GitHub — every week, every file:
👉 github.com/Omk4314/progress-on-python
What Actually Clicked This Week
-
__init__sets state, not creates objects. The object exists before__init__runs. It just gets its starting values there. -
super().__init__()is not optional. Forgetting it in a subclass means the parent's attributes don't exist. The error that follows is confusing until you know why. -
@propertysetters are validation layers. They intercept writes and enforce rules before anything gets stored. The object protects itself. - "is-a" vs "has-a" is the real inheritance question. Once I had that frame, every design decision in the library became obvious.
-
Dunders make objects feel native.
print(account)givingBankAccount(Alice, 85.0)instead of a memory address is a small thing that changes everything about working with your own classes. -
The walrus operator (
:=) is genuinely useful inside property setters — assign the regex match and test it in one expression.
What I Want to Learn Next
Week 6 is coming and I can already see what's missing:
- Abstract base classes — enforcing that subclasses implement certain methods
-
Class methods vs static methods vs instance methods — I've touched
@classmethodbut not the full picture - APIs — pulling live data from the internet into Python objects
-
pytest— writing real tests, not just amain()with print statements - A bigger project — something that uses multiple classes across multiple files with real data
Five weeks in. The code is starting to have architecture.
Drop your Week 5 projects in the comments — I want to see how others are tackling OOP for the first time.
See you in Week 6. 🐍
Week 5 complete. Objects everywhere. No going back.
Top comments (0)