Welcome to Day 2! Today we focus on two core pillars of Object-Oriented Programming:
- Encapsulation: Hiding internal state and requiring all interaction to go through performing validation or controlled methods.
- 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) │
└──────────────┴───────────────┴──────────────────────────────────────────────┘
-
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
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
💻 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.
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
@abstractmethoddefinitions, 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 │
└───────────────────────┘ └───────────────────────┘ └───────────────────────┘
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
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)
Step 3: Run & Verify
uv run payment_system.py
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...
Top comments (0)