OOP has been taught so mechanically for so long — “encapsulation, inheritance, polymorphism, abstraction,” memorized like a multiplication table — that a lot of developers can recite the four pillars without really understanding why any of them matter in real code.
Encapsulation: Hiding Implementation Details
class BankAccount:
def __init__(self, balance=0):
self._balance = balance # convention: "protected", don't touch directly
def deposit(self, amount):
if amount <= 0:
raise ValueError("Deposit must be positive")
self._balance += amount
def get_balance(self):
return self._balance
The point isn’t hiding data for its own sake — it’s that deposit() can enforce the rule “no negative deposits” in exactly one place, instead of trusting every piece of calling code to remember to check.
Inheritance: Sharing Behavior, Carefully
class Vehicle:
def __init__(self, make, model):
self.make, self.model = make, model
def describe(self):
return f"{self.make} {self.model}"
class ElectricCar(Vehicle):
def __init__(self, make, model, battery_kwh):
super().__init__(make, model)
self.battery_kwh = battery_kwh
def describe(self):
return f"{super().describe()} ({self.battery_kwh}kWh battery)"
Inheritance is easy to overuse — modern guidance generally favors composition (an object containing another) over deep inheritance hierarchies, which tend to become fragile as a codebase grows.
Polymorphism: Same Interface, Different Behavior
def print_description(vehicle: Vehicle):
print(vehicle.describe()) # works for Vehicle OR any subclass
print_description(ElectricCar("Tesla", "Model 3", 75))
Why This Still Matters Alongside Functional Style
Modern codebases increasingly blend OOP with functional patterns rather than treating them as opposing philosophies — objects for stateful, identity-bearing things (a database connection, a user session), plain functions and immutable data for everything else. Knowing OOP well makes that blend a deliberate choice rather than a default.