DEV Community

Thiruvengadam Sakthivel
Thiruvengadam Sakthivel

Posted on

#02 – Encapsulation & Abstraction in Python

Welcome to Day 2! Today we focus on two core pillars of Object-Oriented Programming:

  1. Encapsulation: Hiding internal state and requiring all interaction to go through performing validation or controlled methods.
  2. Abstraction: Hiding complex implementation details and exposing only a clean, simplified interface to the user.

1. Access Modifiers: Public, Protected, & Private 🛡️

Python does not have strict enforcement keywords like public or private found in Java or C++. Instead, it uses naming conventions and name mangling to communicate intent and protect internal data.

┌─────────────────────────────────────────────────────────────────────────────┐
│                          ACCESS MODIFIERS IN PYTHON                         │
├──────────────┬───────────────┬──────────────────────────────────────────────┤
│ Level        │ Syntax        │ Scope / Intended Access                      │
├──────────────┼───────────────┼──────────────────────────────────────────────┤
│ Public       │ self.name     │ Accessible anywhere (inside & outside class) │
│ Protected    │ self._balance │ Internal & subclasses only (Convention)      │
│ Private      │ self.__pin    │ Class internal only (Triggers Name Mangling) │
└──────────────┴───────────────┴──────────────────────────────────────────────┘

Enter fullscreen mode Exit fullscreen mode
  • Public (self.name): Fully accessible from anywhere.
  • Protected (self._balance): Single leading underscore. Signals to developers: "This is internal—do not mutate or access outside this class or its subclasses."
  • Private (self.__pin): Double leading underscore. Triggers name mangling (_ClassName__attribute), making it hard to accidentally access or override outside the class.

💻 Code Example: Access Modifiers & Name Mangling

class SecureVault:
    def __init__(self, owner: str, balance: float, pin_code: str):
        self.owner = owner          # Public
        self._balance = balance      # Protected (convention)
        self.__pin = pin_code        # Private (name mangled)

    def verify_pin(self, pin: str) -> bool:
        return self.__pin == pin

vault = SecureVault("Alice", 50000.0, "9876")

# Public access works as expected
print(vault.owner)        # Output: Alice

# Protected access works, but violates Python conventions
print(vault._balance)     # Output: 50000.0

# Private access raises an AttributeError directly
try:
    print(vault.__pin)
except AttributeError as e:
    print("Direct private access blocked:", e)

# Accessing private attribute via Python's name-mangled name
print("Mangled PIN access:", vault._SecureVault__pin)  # Output: 9876

Enter fullscreen mode Exit fullscreen mode

2. Getters & Setters (@property) ⚙️

Instead of writing verbose Java-style methods like get_balance() and set_balance(), Python provides the @property decorator. It allows you to access methods like regular attributes while giving you full control over validation, read-only constraints, and dynamic computation.

       GETTER                                     SETTER
  user.balance ───────► [@property] ───────► Returns internal self._balance
                             │
  user.balance = 100 ────────┴──────────────► [@balance.setter] ──► Validates > 0

Enter fullscreen mode Exit fullscreen mode

💻 Code Example: Controlled Access with @property

class BankAccount:
    def __init__(self, owner: str, initial_balance: float):
        self.owner = owner
        self._balance = initial_balance  # Internal state

    # Getter: Exposes balance as a read-only property
    @property
    def balance(self) -> float:
        return round(self._balance, 2)

    # Setter: Validates new values before updating balance
    @balance.setter
    def balance(self, value: float) -> None:
        if value < 0:
            raise ValueError("Balance cannot be negative.")
        print(f"Balance updated from ${self._balance:.2f} to ${value:.2f}")
        self._balance = value

acc = BankAccount("Charlie", 250.00)

# Calls the getter (looks like an attribute)
print(f"Current Balance: ${acc.balance}")

# Calls the setter (validates input)
acc.balance = 300.00  # Output: Balance updated from $250.00 to $300.00

# Invalid update throws an error
try:
    acc.balance = -50.00
except ValueError as err:
    print("Validation Error:", err)  # Output: Balance cannot be negative.

Enter fullscreen mode Exit fullscreen mode

3. Abstraction & Abstract Base Classes (abc) 🎨

Abstraction means hiding complex underlying details and only defining the contract (methods) that a class must provide.

Python achieves abstraction using the abc module (Abstract Base Classes) and the @abstractmethod decorator:

  • An Abstract Class cannot be instantiated directly.
  • Any concrete subclass must implement all @abstractmethod definitions, or Python will raise an error at instantiation time.
                       ┌────────────────────────────────┐
                       │  «Abstract Base Class»         │
                       │  PaymentProcessor              │
                       ├────────────────────────────────┤
                       │  + process_payment(amount)     │
                       │  + refund_payment(txn_id)      │
                       └───────────────┬────────────────┘
                                       │
            ┌──────────────────────────┼──────────────────────────┐
            ▼                          ▼                          ▼
┌───────────────────────┐  ┌───────────────────────┐  ┌───────────────────────┐
│ CreditCardProcessor   │  │ PayPalProcessor       │  │ CryptoProcessor       │
├───────────────────────┤  ├───────────────────────┤  ├───────────────────────┤
│ Implements methods    │  │ Implements methods    │  │ Implements methods    │
└───────────────────────┘  └───────────────────────┘  └───────────────────────┘

Enter fullscreen mode Exit fullscreen mode

4. Practice Challenge: Design a Payment System 💳

Here is a enterprise-ready payment system architecture built with abstract base classes, private encapsulation, and input validation properties.

Step 1: Initialize Your Workspace

uv init oop_day2 && cd oop_day2
touch payment_system.py

Enter fullscreen mode Exit fullscreen mode

Step 2: Implement payment_system.py

# payment_system.py
from abc import ABC, abstractmethod
from typing import Dict, Any
import uuid


# ==========================================
# 1. ABSTRACT BASE CLASS (INTERFACE CONTRACT)
# ==========================================
class PaymentProcessor(ABC):
    """Abstract base class establishing the contract for all payment gateways."""

    def __init__(self, provider_name: str):
        self.provider_name = provider_name

    @abstractmethod
    def process_payment(self, amount: float) -> Dict[str, Any]:
        """Processes a payment transaction. Must be overridden by subclasses."""
        pass

    @abstractmethod
    def refund_payment(self, transaction_id: str, amount: float) -> bool:
        """Refunds a previous transaction. Must be overridden by subclasses."""
        pass


# ==========================================
# 2. CONCRETE IMPLEMENTATION: CREDIT CARD
# ==========================================
class CreditCardProcessor(PaymentProcessor):
    def __init__(self, merchant_id: str, api_key: str):
        super().__init__("Credit Card Gateway")
        self.merchant_id = merchant_id
        self.__api_key = api_key  # Private API key

    def process_payment(self, amount: float) -> Dict[str, Any]:
        if amount <= 0:
            return {"success": False, "error": "Invalid payment amount."}

        txn_id = f"CC-{uuid.uuid4().hex[:8].upper()}"
        print(f"[{self.provider_name}] Processing ${amount:.2f} via Merchant ID: {self.merchant_id}...")

        return {
            "success": True,
            "transaction_id": txn_id,
            "amount": amount,
            "provider": self.provider_name
        }

    def refund_payment(self, transaction_id: str, amount: float) -> bool:
        print(f"[{self.provider_name}] Refunding ${amount:.2f} on Transaction #{transaction_id}...")
        return True


# ==========================================
# 3. CONCRETE IMPLEMENTATION: PAYPAL
# ==========================================
class PayPalProcessor(PaymentProcessor):
    def __init__(self, client_email: str):
        super().__init__("PayPal System")
        self._client_email = client_email  # Protected attribute

    def process_payment(self, amount: float) -> Dict[str, Any]:
        if amount <= 0:
            return {"success": False, "error": "Invalid payment amount."}

        txn_id = f"PP-{uuid.uuid4().hex[:8].upper()}"
        print(f"[{self.provider_name}] Charging ${amount:.2f} to account {self._client_email}...")

        return {
            "success": True,
            "transaction_id": txn_id,
            "amount": amount,
            "provider": self.provider_name
        }

    def refund_payment(self, transaction_id: str, amount: float) -> bool:
        print(f"[{self.provider_name}] Issuing PayPal refund for ${amount:.2f} to {self._client_email}...")
        return True


# ==========================================
# 4. UNIFIED PAYMENT SERVICE (ABSTRACTION IN ACTION)
# ==========================================
class CheckoutService:
    """Consumes any PaymentProcessor through abstraction without knowing inner mechanics."""

    def __init__(self, processor: PaymentProcessor):
        self.processor = processor

    def checkout(self, amount: float) -> None:
        print("\n--- Initiating Checkout ---")
        result = self.processor.process_payment(amount)

        if result["success"]:
            print(f"✓ Checkout Successful! Txn ID: {result['transaction_id']}")
        else:
            print(f"✗ Checkout Failed: {result.get('error')}")


# ==========================================
# TEST EXECUTION SUITE
# ==========================================
if __name__ == "__main__":
    # Attempting to instantiate abstract class raises TypeError
    try:
        base = PaymentProcessor("Generic")
    except TypeError as err:
        print("Abstract Class Guard Working:")
        print(f"  --> {err}\n")

    # Instantiate concrete processors
    cc_gateway = CreditCardProcessor(merchant_id="MERCH-8821", api_key="secret_live_key")
    paypal_gateway = PayPalProcessor(client_email="billing@corporate.org")

    # Process transactions dynamically via CheckoutService
    cart_1 = CheckoutService(cc_gateway)
    cart_1.checkout(149.99)

    cart_2 = CheckoutService(paypal_gateway)
    cart_2.checkout(89.50)

    # Trigger a refund
    cc_gateway.refund_payment("CC-9A2F1B0C", 149.99)

Enter fullscreen mode Exit fullscreen mode

Step 3: Run & Verify

uv run payment_system.py

Enter fullscreen mode Exit fullscreen mode

Output Summary

Abstract Class Guard Working:
  --> Can't instantiate abstract class PaymentProcessor without an implementation for abstract methods 'process_payment', 'refund_payment'


--- Initiating Checkout ---
[Credit Card Gateway] Processing $149.99 via Merchant ID: MERCH-8821...
✓ Checkout Successful! Txn ID: CC-7B9F10A2

--- Initiating Checkout ---
[PayPal System] Charging $89.50 to account billing@corporate.org...
✓ Checkout Successful! Txn ID: PP-3E41D8B9

[Credit Card Gateway] Refunding $149.99 on Transaction #CC-9A2F1B0C...

Enter fullscreen mode Exit fullscreen mode

Top comments (0)