Lesson 17 — Object-Oriented Programming

OOP in Python

Python OOP uses class and __init__. self is the explicit object reference. Special methods (__str__, __len__, __eq__) make your objects behave like built-in types.

PYTHON — Class definition
class Learner:
    """A learner enrolled in the Your IT Tutor programme."""

    # Class attribute — shared by all instances:
    institution = "Your IT Tutor"

    def __init__(self, name: str, age: int, course: str, mark: float = 0.0):
        # Instance attributes — unique to each object:
        self.name   = name
        self.age    = age
        self.course = course
        self.mark   = mark

    def introduce(self):
        """Print a self-introduction."""
        print(f"I am {self.name}, aged {self.age}, "
              f"studying {self.course} at {self.institution}.")

    def grade_label(self) -> str:
        if self.mark >= 80: return "Distinction"
        if self.mark >= 60: return "Merit"
        if self.mark >= 50: return "Pass"
        return "Not yet competent"

# Creating objects:
l1 = Learner("Thandi", 21, "Python", 74.5)
l2 = Learner("Sipho",  24, "Java",   81.0)
l1.introduce()
l2.introduce()
print(l1.grade_label())

Special (Dunder) Methods

Dunder methods let your objects integrate with Python's built-in operations — printing, comparison, length, iteration.

PYTHON — Dunder methods
class Learner:
    def __init__(self, name, mark):
        self.name = name
        self.mark = mark

    def __str__(self):
        """Called by print() and str()"""
        return f"Learner({self.name}, {self.mark}%)"

    def __repr__(self):
        """Called in the REPL and for debugging"""
        return f"Learner(name={self.name!r}, mark={self.mark!r})"

    def __eq__(self, other):
        """Called by =="""
        if not isinstance(other, Learner):
            return NotImplemented
        return self.name == other.name and self.mark == other.mark

    def __lt__(self, other):
        """Called by < — enables sorting"""
        return self.mark < other.mark

    def __len__(self):
        """Called by len() — returns meaningful length"""
        return len(self.name)

l1 = Learner("Thandi", 74.5)
l2 = Learner("Sipho",  81.0)
l3 = Learner("Thandi", 74.5)

print(l1)             # Learner(Thandi, 74.5%)  — __str__
print(l1 == l3)       # True  — __eq__
print(l1 < l2)        # True  — __lt__
print(sorted([l2,l1])) # sorted by mark  — __lt__
print(len(l1))        # 6 — __len__

Practice Task

Your Turn

Build a BankAccount class with: account_number (auto-generated, read-only), holder_name, balance (float). Implement __str__, __repr__, __eq__ (by account_number), __iadd__ (deposit with +=), __isub__ (withdraw with -=, raise InsufficientFundsError if insufficient). Test all operations.

Common Mistakes

  • Forgetting self in the method definition — def introduce(): instead of def introduce(self): raises TypeError on call.
  • Accessing self.name inside a method as just name — NameError.
  • Mutable default arguments in __init__ — use None.
  • Confusing class attributes and instance attributes — class attrs are shared, instance attrs are per-object.

Professional Tip

Implement __repr__ to produce a string that could recreate the object: Learner(name='Thandi', mark=74.5). This is invaluable for debugging — it is what Python shows in the REPL and in error messages.

Mini Quiz

What is the purpose of self in Python instance methods?