Skip to main content

Command Palette

Search for a command to run...

From Classes to Polymorphism: My OOP Learning Journey in Python

Published
10 min read
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.

  1. 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 BankAccount that had two main behaviors:

    • deposit() → to add money into the account.

    • withdraw() → to remove money if funds are sufficient.

  • Then, I created a subclass SavingsAccount that inherited from BankAccount but added an extra feature:

    • add_interest() → calculates and applies interest to the current balance.

Here’s what I learned from this exercise:

  1. Encapsulation:
    By wrapping data (owner, balance) and functionality (deposit, withdraw) inside a class, the account became like a “blueprint” for real bank accounts.

  2. Inheritance:
    The SavingsAccount class reused all the logic from BankAccount but still allowed me to add new features like interest. This showed how child classes can extend parent classes without rewriting code.

  3. Polymorphism (in action):
    Even though both BankAccount and SavingsAccount can deposit or withdraw, the subclass could have its own specialized methods.

  4. User Interaction:
    I also added a simple menu using a while True loop. 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:

  1. 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")
  1. 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 for loop.

    def __iter__(self):
        return self
  1. 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 StopIteration to end the loop.

    def __next__(self):
        line = self.file.readline()
        if line:
            return line.strip()
        else:
            self.file.close()
            raise StopIteration
  1. 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 for loop is calling __next__() until StopIteration is raised.

Key Takeaways

  • Iterators give us fine-grained control over how data is accessed.

  • Using __iter__() and __next__() made me realize how Python’s for loops 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.

  1. Abstract Base Class

    • I created a base class Shape using Python’s ABC module.

    • 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
  1. Subclasses with Specific Implementations

    • Rectangle implemented area() as width * height.

    • Circle implemented area() 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)
  1. 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.

  1. 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

  1. Outer function

    • Defines a variable message = "Hi".

    • This variable lives in the enclosing scope.

  2. Inner function

    • Declares nonlocal message, which tells Python:
      “Don’t treat message as local — look one level up (enclosing scope) and modify that instead.”

    • Updates the value of message to "what's up".

  3. Output

     Updated message: what's up
     Outer message: what's up
    
    • The change inside inner() persisted outside because nonlocal linked the variable back to the enclosing scope.

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 to message inside inner() 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.

  1. Library Management System

o practice OOP concepts, I built a Library Management System that demonstrates:

  • InheritanceBookEBook, PrintedBook

  • Abstract ClassesMember (abstract) → StudentMember, TeacherMember

  • Polymorphism → Overriding get_description() and borrow_book()

  • Encapsulation → Private attributes like __library_name

  • Class Attributestotal_borrowed to track all borrowing activity

  • Iterators → Iterating through borrowed books or library collection

How It Works

  1. Book Classes

    • Base Book contains title, author, year, and borrow count.

    • EBook adds file size and format.

    • PrintedBook adds weight and dimensions.

    • Each overrides get_description() for polymorphism.

  2. Member Classes

    • Member is an abstract class (cannot be directly instantiated).

    • Defines borrowing/returning logic, an iterator over borrowed books, and a simple badge system .

    • StudentMember and TeacherMember inherit with different borrowing limits (3 vs 5).

  3. 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