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.
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
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