From Classes to Polymorphism: My OOP Learning Journey in Python

Object-Oriented Programming is a paradigm that organizes code into classes (blueprints) and objects (instances of those blueprints). It allows us to model real-world entities more naturally in code.
Objects and Classes
A class defines the structure (attributes) and behavior (methods), while an object is a specific instance of that class.
Inheritance
Inheritance enables a class to reuse code from another class, reducing duplication.
Parent and Child Classes
The parent class provides general behavior, while child classes extend or override it to create specialized versions.
Iterators
Iterators allow us to loop through objects systematically. By defining the __iter__() and __next__() methods, we can make custom classes iterable.
Polymorphism
Polymorphism lets different classes implement methods with the same name, but with different behavior. This makes code more flexible and extensible.
Function Polymorphism
Same function can handle different types of inputs.
Class Polymorphism
Different classes can define the same method (get_description()) with unique behavior.
Inheritance Polymorphism
Child classes override parent methods to provide specialized functionality.
Scope and the LEGB Rule
Scope determines where a variable is accessible. Python resolves variables using the LEGB rule:
Local – inside the current function
Enclosing – in the outer (enclosing) functions
Global – defined at the module level
Built-in – reserved names/functions in Python
The nonlocal and global keywords help bridge these scopes when modification is needed.
Projects and Exercises
The beauty of learning Python is undeniably in getting your hands dirty. This week, There were a lot of Challenging and mentally engaging tasks in store for us this week. Each ot the tasks were designed to give us a chance to practice on what we have been taught through the week.
Bank Account Program
This week, I practiced OOP by building a simple Bank Account program in Python. The goal was to understand how classes, inheritance, and methods work together in a real-world scenario.

I started with a base class
BankAccountthat had two main behaviors:deposit()→ to add money into the account.withdraw()→ to remove money if funds are sufficient.
Then, I created a subclass
SavingsAccountthat inherited fromBankAccountbut added an extra feature:add_interest()→ calculates and applies interest to the current balance.
Here’s what I learned from this exercise:
Encapsulation:
By wrapping data (owner,balance) and functionality (deposit,withdraw) inside a class, the account became like a “blueprint” for real bank accounts.Inheritance:
TheSavingsAccountclass reused all the logic fromBankAccountbut still allowed me to add new features like interest. This showed how child classes can extend parent classes without rewriting code.Polymorphism (in action):
Even though bothBankAccountandSavingsAccountcandepositorwithdraw, the subclass could have its own specialized methods.
User Interaction:
I also added a simple menu using awhile Trueloop. This made the program interactive, allowing the user to:Deposit money
Withdraw money
Add interest
Check balance
Exit the app
This project made the theory of OOP feel practical. I wasn’t just reading definitions — I could see how classes and inheritance help structure code in a way that’s reusable, expandable, and closer to how things work in real life (e.g., a bank account system).
Next step for me is to explore how this program could be scaled:
Adding multiple accounts
Handling errors more gracefully
Storing data so balances don’t reset when the program ends
FileLine Reader Iterator
Another concept I practiced this week was iterators by building a custom class called FileLineReader. The idea was to mimic how Python naturally loops over files, but to do it manually so I could understand what happens under the hood. Here’s what I learned:

Initialization with
__init__()The class takes a filename and opens the file in read mode.
This sets things up so the object is ready to start reading.
def __init__(self, filename):
self.file = open(filename, "r")
Making the class iterable with
__iter__()The
__iter__()method returns the iterator object itself (self).This is what allows us to use the object inside a
forloop.
def __iter__(self):
return self
Controlling iteration with
__next__()Each time Python asks for the “next” item, the
__next__()method is called.The method reads one line from the file at a time.
If a line exists, it returns the line (with
.strip()to clean it).If no more lines remain, it closes the file and raises
StopIterationto end the loop.
def __next__(self):
line = self.file.readline()
if line:
return line.strip()
else:
self.file.close()
raise StopIteration
Testing the Iterator
reader = FileLineReader("sample.txt") for line in reader: print(line)Now, the class works exactly like Python’s built-in file iteration: it reads each line one by one.
Behind the scenes, the
forloop is calling__next__()untilStopIterationis raised.
Key Takeaways
Iterators give us fine-grained control over how data is accessed.
Using
__iter__()and__next__()made me realize how Python’sforloops really work internally.This pattern is useful when working with large datasets or streams of information where you don’t want to load everything into memory at once.
For me, the “aha moment” was seeing how a simple class could replicate what Python does automatically with files. It showed me why iterators are so important in ML — they let you process data step by step, especially when dealing with huge datasets that can’t fit in memory.
3.Abstract Classes and Polymorphism
To deepen my understanding of OOP, I explored how Python uses abstract base classes (ABC) and polymorphism. I built a small project with geometric shapes to see these concepts in action.
Abstract Base Class
I created a base class
Shapeusing Python’sABCmodule.It had an abstract method
area()— meaning every subclass must implement it.This enforced consistency: all shapes must be able to calculate their area.
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
Subclasses with Specific Implementations
Rectangleimplementedarea()aswidth * height.Circleimplementedarea()using the formula πr².Both inherited from
Shape, but each gave its own unique calculation.
class Rectangle(Shape):
def area(self):
return self.width * self.height
class Circle(Shape):
def area(self):
return math.pi * (self.radius ** 2)
Polymorphism in Action
I stored different shapes (rectangles and circles) in the same list.
Without checking their types, I looped through and called
.area().Thanks to polymorphism, Python automatically used the right implementation for each shape.
for shape in shapes:
print(f"Area: {shape.area():.2f}")
Output looked like:
Area: 24.00
Area: 28.27
Area: 10.00
Area: 3.14
Key Takeaways
Abstract classes provide a blueprint that guarantees certain methods exist in subclasses.
Polymorphism means I don’t need to know what kind of shape I’m working with — as long as it’s a
Shape, I can call.area()and trust it will work.This mirrors real-world ML workflows: we often design code to handle different data types or models interchangeably, relying on a shared interface.
This exercise made polymorphism feel less abstract and more practical. It showed me how OOP principles make code cleaner, reusable, and extensible.
Scope
To wrap up my OOP + scope practice this week, I experimented with Python’s LEGB rule (Local, Enclosed, Global, Built-in) and the nonlocal keyword.
Here’s the example I worked with:
def outer():
message = "Hi"
def inner():
nonlocal message
message = "what's up"
print('Updated message:', message)
inner()
print('Outer message:', message)
outer()
Step-by-step Understanding
Outer function
Defines a variable
message = "Hi".This variable lives in the enclosing scope.
Inner function
Declares
nonlocal message, which tells Python:
“Don’t treatmessageas local — look one level up (enclosing scope) and modify that instead.”Updates the value of
messageto"what's up".
Output
Updated message: what's up Outer message: what's up- The change inside
inner()persisted outside becausenonlocallinked the variable back to the enclosing scope.
- The change inside
Key Takeaways
Python resolves variables using LEGB:
Local → inside the current function
Enclosed → in parent (nested) functions
Global → defined at the module level
Built-in → reserved Python keywords/functions
Without
nonlocal, assigning tomessageinsideinner()would have created a new local variable instead of updating the outer one.This concept is powerful when working with closures or functions inside functions — it lets inner functions modify state from their enclosing scope.
Library Management System
o practice OOP concepts, I built a Library Management System that demonstrates:
Inheritance →
Book→EBook,PrintedBookAbstract Classes →
Member(abstract) →StudentMember,TeacherMemberPolymorphism → Overriding
get_description()andborrow_book()Encapsulation → Private attributes like
__library_nameClass Attributes →
total_borrowedto track all borrowing activityIterators → Iterating through borrowed books or library collection
How It Works
Book Classes
Base
Bookcontains title, author, year, and borrow count.EBookadds file size and format.PrintedBookadds weight and dimensions.Each overrides
get_description()for polymorphism.
Member Classes
Memberis an abstract class (cannot be directly instantiated).Defines borrowing/returning logic, an iterator over borrowed books, and a simple badge system .
StudentMemberandTeacherMemberinherit with different borrowing limits (3 vs 5).
Library Class
Tracks available and borrowed books.
Provides
add_book()and an__iter__()for looping through available books.Implements
most_popular_book()using a simple popularity counter (times_borrowed).
Example Demo (Usage)
I wrapped the library system into a __main__ demo to simulate how books and members interact in a real scenario.
# DEMO
if __name__ == "__main__":
# Create Library
my_library = Library("Central Library")
# Add some books
book1 = PrintedBook("Things Fall Apart", "Chinua Achebe", 1958, "500g", "8x5 in")
book2 = EBook("Python 101", "Michael Driscoll", 2020, "5MB", "PDF")
book3 = PrintedBook("Half of a Yellow Sun", "Chimamanda Ngozi Adichie", 2006, "600g", "9x6 in")
my_library.add_book(book1)
my_library.add_book(book2)
my_library.add_book(book3)
# Create Members
student = StudentMember("Nanfe", "S001", "Grade 10")
teacher = TeacherMember("Mr. Yarnap", "T001", "Mathematics")
# Borrowing
print(student.borrow(book1, my_library)) # should work
print(teacher.borrow(book2, my_library)) # should work
print(student.borrow(book2, my_library)) # should fail (already borrowed)
# Show borrowed books
print("Nanfe's borrowed books:", student.get_borrowed_books())
print("Mr. Yarnap's borrowed books:", teacher.get_borrowed_books())
# Return book
print(student.return_book(book1, my_library))
# Show most popular book
print(my_library.most_popular_book())
# Search functionality
print("Search results for 'Python':", my_library.search_books("Python"))
print("Search results for 'Achebe':", my_library.search_books("Achebe"))
# Badges
print("Nanfe's badge:", student.get_badge())
print("Yarnap's badge:", teacher.get_badge())
# Test library name getter & setter
print("Library name:", my_library.get_library_name())
my_library.set_library_name("Community Library")
print("Updated Library name:", my_library.get_library_name())
# Total borrowed count
print("Total books borrowed by all members:", Member.total_borrowed)
Output
Nanfe borrowed 'Things Fall Apart'.
Mr. Yarnap borrowed 'Python 101'.
'Python 101' is not available.
Nanfe's borrowed books: ['Things Fall Apart']
Mr. Yarnap's borrowed books: ['Python 101']
Nanfe returned 'Things Fall Apart'.
Most popular book: 'Things Fall Apart' (1 borrows).
Search results for 'Python': ["Python 101"]
Search results for 'Achebe': ["Things Fall Apart"]
Nanfe's badge: 📚 Beginner Reader Badge!
Yarnap's badge: 📚 Beginner Reader Badge!
Library name: Central Library
Updated Library name: Community Library
Total books borrowed by all members: 1
What This Demonstrates
Borrowing/Returning with proper limits and error handling.
Most Popular Book tracking based on times borrowed.
Search Functionality to find books by title/author.
Member Badges (gamified reading levels).
Encapsulation → secure access to library name via getter/setter.
Class Attributes (
total_borrowed) tracking usage across all membe
Key Learning Points
Abstract classes help enforce rules (
Member.borrow_book()must be overridden).Polymorphism allows the same method (
get_description) to behave differently depending on the class.Encapsulation (like
__library_name) protects attributes from direct modification.Iterators make classes feel more "Pythonic" by supporting
for ... in ....Adding fun features like badges and most popular book made the project more engaging and practical.
This project tied together inheritance, polymorphism, scope, and iterators into one cohesive system. It gave me a hands-on understanding of how OOP principles apply in real-world applications.
Overall, this week was a turning point in my journey — moving from basic Python syntax into the structured world of Object-Oriented Programming. By experimenting with inheritance, polymorphism, iterators, and scope through the library project, I now see how these concepts form the backbone of scalable software systems. I’m excited to carry these lessons forward as I continue my internship, knowing that each week builds a stronger foundation for the machine learning work ahead

