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?
@property turns a method into a readable property. Pair it with @property_name.setter for a setter and @property_name.deleter for a deleter.