Lesson 18 — Deeper OOP

Properties and Class Methods

@property replaces explicit getters/setters with attribute-style access. @classmethod and @staticmethod handle class-level and utility logic cleanly.

PYTHON — @property
class Learner:
    def __init__(self, name: str, age: int):
        self._name = name  # _name is the backing attribute
        self.age   = age   # uses the property setter

    @property
    def name(self) -> str:
        """Getter — called when you read learner.name"""
        return self._name

    @name.setter
    def name(self, value: str):
        """Setter — called when you write learner.name = ..."""
        if not value or not value.strip():
            raise ValueError("Name cannot be empty.")
        self._name = value.strip().title()

    @property
    def age(self) -> int:
        return self._age

    @age.setter
    def age(self, value: int):
        if value < 15 or value > 80:
            raise ValueError(f"Age {value} is invalid for a learner.")
        self._age = value

    @property
    def is_adult(self) -> bool:
        """Read-only computed property — no setter."""
        return self._age >= 18

# Usage — looks like field access but runs validation:
l = Learner("thandi mokoena", 21)  # name is normalised to title case
print(l.name)    # Thandi Mokoena
print(l.is_adult) # True
l.age = 22

try:
    l.age = -5
except ValueError as e:
    print(e)

@classmethod and @staticmethod

PYTHON — classmethod and staticmethod
class Learner:
    _count = 0
    _id_counter = 1000

    def __init__(self, name, course):
        self.name   = name
        self.course = course
        self.id     = Learner._id_counter
        Learner._id_counter += 1
        Learner._count      += 1

    @classmethod
    def count(cls) -> int:
        """Factory or counter method — receives the class as first arg."""
        return cls._count

    @classmethod
    def from_csv_row(cls, row: str):
        """Alternative constructor — create from a CSV string."""
        parts = row.split(",")
        return cls(parts[0].strip(), parts[1].strip())

    @staticmethod
    def is_valid_mark(mark: float) -> bool:
        """Utility — no access to class or instance needed."""
        return 0.0 <= mark <= 100.0

# Usage:
l1 = Learner("Thandi", "Python")
l2 = Learner.from_csv_row("Sipho, Java")  # alternative constructor
print(Learner.count())                     # 2
print(Learner.is_valid_mark(74.5))         # True
print(Learner.is_valid_mark(150))          # False

Practice Task

Your Turn

Add @property to your BankAccount class for balance (read-only, validates on set), account_type computed from balance ("Premium" if > 50000, else "Standard"), and a @classmethod from_dict(cls, d) that creates an account from a dictionary. Test all.

Common Mistakes

  • Forgetting @property on the getter — without it, name is a regular method not a property.
  • @classmethod and @staticmethod are decorators — the @ is required.
  • Using @staticmethod when the method needs class-level data — use @classmethod instead.
  • Property setters must have the exact same name as the getter with @name.setter.

Professional Tip

@classmethod alternative constructors (from_csv_row, from_dict, from_api_response) are one of the cleanest patterns in Python OOP. They make object creation flexible without overloading __init__.

Mini Quiz

What decorator creates a getter property in Python?