OOP Series

Encapsulation: Nobody Gets to Touch Your Venmo Balance Directly

Your Venmo balance is real money.

But think about it for a second. Where does it actually live? On a server somewhere. In a database. In a variable. Something like balance = 342.50.

Can you reach in and change that variable directly? Just set balance = 999999?

You can’t.

You have to go through the designated paths. Want to send money? Tap “Pay”. Want to move it to your bank? Hit “Transfer”. Want to check it? Open the app. And without your PIN, none of it works. You don’t know how it all happens under the hood — and honestly, you don’t need to.

That system has a name: Encapsulation.


1. What is Encapsulation?

Put simply: Encapsulation = Data Hiding + Controlled Access.

It means bundling data (variables) and behavior (methods) together inside a class, and instead of letting anyone touch the data directly, only exposing specific paths to do so.

Think of it like an ATM. You can’t walk into the vault and change your balance. You go through the ATM. The ATM gives you exactly three things: deposit(), withdraw(), get_balance(). What happens inside? Not your concern.

%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#F5F5F5', 'primaryTextColor': '#000', 'primaryBorderColor': '#333', 'lineColor': '#333', 'background': '#fff'}}}%% graph LR classDef box fill:#F5F5F5,color:#000,stroke:#333 U["You (User)"]:::box A["Public Interface deposit() withdraw() getBalance()"]:::box V["Private Data balance account_number pin_hash"]:::box U -->|"only through here"| A A -->|"controlled access"| V U -.->|"direct access blocked"| V

Why does Encapsulation matter?

First, sensitive data stays hidden. Balance, password, card number — none of it is directly readable.

Second, controlled validation. If someone tries to deposit -$500, the method can catch it and block it. The rule lives inside the method.

Third, maintainability. If the internal implementation changes, outside code doesn’t break. Venmo could switch their balance from int to float — you’d never notice.


2. Access Modifiers: How Much Do You Show?

The main tool for implementing Encapsulation is Access Modifiers — they control which parts of a class are visible from the outside.

Three core modifiers:

private: Only accessible within the class itself. Used most often to hide data. Nobody outside can touch it.

protected: Accessible within the class and its subclasses. Useful in inheritance.

public: Visible to everyone. This is your controlled interface.

Simple rule: keep everything private by default, and only make something public when you have to.

class DigitalWallet:
    def __init__(self, owner, initial_balance):
        self.__owner = owner              # private — not visible outside
        self.__balance = initial_balance  # private — cannot be changed directly
        self.__pin = None                 # private

    def get_balance(self):               # public — read only
        return self.__balance

    def deposit(self, amount):           # public — controlled access
        if amount <= 0:
            print("Invalid amount")
            return False
        self.__balance += amount
        return True

    def withdraw(self, amount):          # public — controlled access
        if amount <= 0:
            print("Invalid amount")
            return False
        if amount > self.__balance:
            print("Insufficient balance")
            return False
        self.__balance -= amount
        return True

In Python, the __ prefix makes a field private and blocks direct access. See what happens:

wallet = DigitalWallet("Raian", 1000)

# correct path
print(wallet.get_balance())   # 1000
wallet.deposit(500)
print(wallet.get_balance())   # 1500

# attempt direct access
wallet.__balance = 999999     # this won't work!
print(wallet.get_balance())   # still 1500

3. Getters and Setters: The Controlled Door

The public methods used to read and write private data are called Getters and Setters.

Getter: Read-only access. get_balance() lets you see the balance — but not change it.

Setter: Write access, but with validation baked in. Invalid values never get through.

class BankAccount:
    def __init__(self):
        self.__balance = 0
        self.__account_holder = ""

    # Getter
    def get_balance(self):
        return self.__balance

    # Getter
    def get_account_holder(self):
        return self.__account_holder

    # Setter with validation
    def set_account_holder(self, name):
        if not name or len(name.strip()) == 0:
            raise ValueError("Account holder name cannot be empty")
        self.__account_holder = name.strip()

    # Business method with validation
    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("Deposit amount must be positive")
        self.__balance += amount
        print(f"Deposited {amount}. New balance: {self.__balance}")

    def withdraw(self, amount):
        if amount <= 0:
            raise ValueError("Withdrawal amount must be positive")
        if amount > self.__balance:
            raise ValueError("Insufficient funds")
        self.__balance -= amount
        print(f"Withdrew {amount}. New balance: {self.__balance}")

Because of the setter, account_holder = "" or balance = -500 can never happen. The method catches it first.


Real-World Example: Payment Processor

This one is the most powerful example.

You’re building a payment system. You need to store a customer’s credit card number. But storing the full number is a security risk — you can only keep a masked version: ****-****-****-1234.

Encapsulation is the perfect solution:

class PaymentProcessor:
    def __init__(self, card_number: str, amount: float):
        # raw card number is never stored
        # masked immediately in the constructor
        self.__masked_card = self.__mask_card(card_number)
        self.__amount = amount
        self.__is_processed = False

    def __mask_card(self, card_number: str) -> str:
        # private method — cannot be called from outside
        digits_only = card_number.replace("-", "").replace(" ", "")
        return f"****-****-****-{digits_only[-4:]}"

    def process_payment(self) -> bool:
        if self.__is_processed:
            print("This payment has already been processed.")
            return False

        print(f"Processing ${self.__amount} via card {self.__masked_card}...")
        self.__is_processed = True
        print("Payment successful!")
        return True

    def get_masked_card(self) -> str:
        return self.__masked_card

    def get_amount(self) -> float:
        return self.__amount
%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#F5F5F5', 'primaryTextColor': '#000', 'primaryBorderColor': '#333', 'lineColor': '#333', 'background': '#fff'}}}%% graph TD classDef box fill:#F5F5F5,color:#000,stroke:#333 IN["Input 4111-1111-1111-1234 $500"]:::box PP["PaymentProcessor (constructor runs)"]:::box PRIV["Private Data __masked_card: ****-****-****-1234 __amount: 500 __is_processed: False"]:::box PUB["Public Methods process_payment() get_masked_card() get_amount()"]:::box IN -->|"create"| PP PP -->|"masks and stores"| PRIV PP --> PUB
processor = PaymentProcessor("4111-1111-1111-1234", 500)

# correct path
processor.process_payment()
# Processing $500 via card ****-****-****-1234...
# Payment successful!

print(processor.get_masked_card())  # ****-****-****-1234

# raw card number is never accessible
# processor.__mask_card() won't work (private method)

Here’s the beauty of this design: the raw card number is masked the moment it enters the object. If someone later inspects the object, logs it, or debugs it, they only ever see the masked version. The original number is gone for good.


Summary

In the story In code
Venmo balance can’t be changed directly Private variable
The ATM’s three buttons Public methods (controlled interface)
Nothing works without your PIN Access modifier: private
Cashier knows but doesn’t tell Getter without setter
Blocking a negative deposit Setter with validation
Storing only the masked card number Encapsulation for security

Keep data private. Give access only through methods.

Validation inside a method means invalid state can never exist.

When implementation changes, outside code stays intact — that’s maintainability.


Encapsulation hides the details. But what if you could hide the entire complexity of a class and expose only the essential part? That idea has a name: Abstraction.

Next in the OOP series: Abstraction — the art of hiding complexity