Skip to content

Encapsulation

"""
Encapsulation is the bundling of data (attributes) and methods that operate on that data within a single unit (class)
while restricting direct access to some of the object's components.

Data Hiding: private/protected attributes,
Controlled Access: getter/setter/property,
Interface Design : exposing only what's necessary for external use
"""


class BankAccount:
    """Demonstrates Encapsualtion principles"""

    def __init__(self, account_number: str, initial_balance: float = 0.0) -> None:
        # public
        self.account_num = account_number
        # protected
        self._account_type = "Savings"
        # private
        self.__balance = initial_balance
        self.__transaction_history = []
        if initial_balance < 0:
            raise ValueError("Initial balance can't be negative")

    def deposit(self, amount: float) -> None:
        """Deposits money into account"""
        if amount <= 0:
            raise ValueError("Deposit can't be negative")
        self.__balance += amount
        self.__add_transaction(f"Deposit : ${amount:.2f}")
        print(f"Deposited ${amount:.2f}. New balance: ${self.__balance:.2f}")

    def withdraw(self, amount: float) -> bool:
        if amount <= 0:
            raise ValueError("Can't withdraw")
        if amount > self.__balance:
            print("Insufficient Funds")

        self.__balance -= amount
        self.__add_transaction(f"Withdraw: - ${amount:.2f}")
        print(f"Withdrew ${amount:.2f}. New balance: ${self.__balance:.2f}")
        return True

    # private methods for add transaction
    def __add_transaction(self, transaction: str) -> None:
        """Private method to record transactions"""
        from datetime import datetime

        timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        self.__transaction_history.append(f"[{timestamp}] {transaction}")

    # public method that use private data
    def get_transaction_history(self, n_last: int = 5) -> list:
        return self.__transaction_history[-n_last:]

    def __str__(self) -> str:
        return f"BankAccount({self.account_num}): ${self.__balance:2f}"

    # getters and setters for private variables
    # Property for controlled access to balance (read-only)
    @property
    def balance(self) -> int:
        return self.__balance

    @property
    def account_type(self) -> str:
        return self._account_type

    @account_type.setter
    def account_type(self, val: str) -> None:
        valid_types = ["Savings", "Business", "Checking"]
        if val not in valid_types:
            raise ValueError(f"Account type must be one of: {valid_types}")
        self._account_type = val


if __name__ == "__main__":
    account = BankAccount("ACC-12345", 1000.0)

    # Public interface usage
    print(f"Initial balance: ${account.balance:.2f}")  # Property access
    account.deposit(500)
    account.withdraw(200)

    # Controlled access through properties
    account.account_type = "Checking"  # Uses setter with validation
    print(f"Account type: {account.account_type}")

    # This would raise an error due to validation:
    # account.account_type = "InvalidType"

    # Accessing transaction history
    print("Recent transactions:")
    for transaction in account.get_transaction_history():
        print(f"  {transaction}")

    print("\n--- Demonstrating Encapsulation Protection ---")

    # Direct access to private attributes is prevented
    print(f"Balance through property: ${account.balance:.2f}")

    # This would cause an AttributeError:
    # print(account.__balance)  # AttributeError

    # Name mangling makes it harder to access (but not impossible)
    # print(account._BankAccount__balance)  # This would work but breaks encapsulation