Lesson 20 of 25

OOP: Encapsulation & Abstraction

Encapsulation: Private Attributes and Properties

Encapsulation is the practice of hiding internal details of a class and controlling access through public methods. In Python, there are no truly private attributes, but conventions indicate access levels.

A single underscore prefix (_attr) signals 'internal use.' A double underscore prefix (__attr) triggers name mangling, making it harder to access from outside the class. The @property decorator lets you create getter/setter methods that look like attribute access.

Example
class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner         # Public
        self._account_type = "Savings"  # Protected (convention)
        self.__balance = balance   # Private (name mangled)

    @property
    def balance(self):
        """Getter for balance"""
        return self.__balance

    @balance.setter
    def balance(self, amount):
        """Setter with validation"""
        if amount < 0:
            raise ValueError("Balance cannot be negative")
        self.__balance = amount

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("Deposit must be positive")
        self.__balance += amount
        return self.__balance

    def withdraw(self, amount):
        if amount > self.__balance:
            raise ValueError("Insufficient funds")
        self.__balance -= amount
        return self.__balance

account = BankAccount("Alice", 1000)
print(account.balance)       # 1000 (uses @property getter)
account.deposit(500)
print(account.balance)       # 1500
account.withdraw(200)
print(account.balance)       # 1300

# account.__balance          # AttributeError!
# account.balance = -100     # ValueError: Balance cannot be negative
  • public_attr — accessible from anywhere
  • _protected_attr — convention: internal use only (still accessible)
  • __private_attr — name mangled to _ClassName__private_attr
  • @property — turn a method into a read-only attribute
  • @attr.setter — define a setter with validation logic
Try Encapsulation
JavaScript
class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.__balance = balance

    @property
    def balance(self):
        return self.__balance

    def deposit(self, amount):
        self.__balance += amount

    def withdraw(self, amount):
        if amount > self.__balance:
            print("Insufficient funds!")
            return
        self.__balance -= amount

acct = BankAccount("Alice", 1000)
print(f"Balance: ${acct.balance}")
acct.deposit(500)
print(f"After deposit: ${acct.balance}")
acct.withdraw(2000)
Notes
  • Python's philosophy is 'we're all consenting adults.' The underscore conventions are guidelines, not strict enforcement. Trust your team to follow them.

Abstract Classes

An abstract class is a class that cannot be instantiated directly — it serves as a blueprint for other classes. Abstract methods are declared but have no implementation in the base class; subclasses must provide their own implementation.

Python provides the abc module (Abstract Base Classes) to create abstract classes. Any subclass that doesn't implement all abstract methods will raise a TypeError when instantiated.

Example
from abc import ABC, abstractmethod

class Vehicle(ABC):
    def __init__(self, make, model, year):
        self.make = make
        self.model = model
        self.year = year

    @abstractmethod
    def fuel_type(self):
        """Must be implemented by subclasses"""
        pass

    @abstractmethod
    def start(self):
        pass

    # Non-abstract method — inherited as-is
    def info(self):
        return f"{self.year} {self.make} {self.model}"

class ElectricCar(Vehicle):
    def fuel_type(self):
        return "Electric"

    def start(self):
        return "Silently powering on..."

class GasCar(Vehicle):
    def fuel_type(self):
        return "Gasoline"

    def start(self):
        return "Vroom! Engine starting..."

# Cannot instantiate abstract class
# v = Vehicle("Generic", "Car", 2024)  # TypeError!

tesla = ElectricCar("Tesla", "Model 3", 2024)
camry = GasCar("Toyota", "Camry", 2023)

for car in [tesla, camry]:
    print(f"{car.info()} — {car.fuel_type()} — {car.start()}")
Try Abstract Classes
JavaScript
from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

class Circle(Shape):
    def __init__(self, r):
        self.r = r
    def area(self):
        return 3.14159 * self.r ** 2

class Square(Shape):
    def __init__(self, side):
        self.side = side
    def area(self):
        return self.side ** 2

for shape in [Circle(5), Square(4)]:
    print(f"{type(shape).__name__}: {shape.area():.2f}")
Notes
  • Abstract classes define a contract that subclasses must follow. They are a powerful tool for designing clean APIs and ensuring consistency across related classes.