Encapsulation
Encapsulation means bundling attributes and methods together inside a class, and controlling access to them. It's like putting your belongings in a box - you decide what others can see and use.
To demonstrate encapsulation, consider the following example where we want to manage a bank account:
❌ Without Encapsulation
# bank_account.py # Account data is directly accessible and can be modified incorrectly account_balance = 1000 account_owner = "Alice" def withdraw(amount): global account_balance account_balance -= amount print(f"Withdrew ${amount}. New balance: ${account_balance}") # Problem: Anyone can directly change the balance account_balance = -500 # This shouldn't be allowed! withdraw(1000) # This could make balance negative!# bank_account.py # Account data is directly accessible and can be modified incorrectly account_balance = 1000 account_owner = "Alice" def withdraw(amount): global account_balance account_balance -= amount print(f"Withdrew ${amount}. New balance: ${account_balance}") # Problem: Anyone can directly change the balance account_balance = -500 # This shouldn't be allowed! withdraw(1000) # This could make balance negative!
Problem: The balance can be changed directly, even to invalid values like negative numbers. There's no protection or validation.
✅ With Encapsulation
# bank_account.py class BankAccount: def __init__(self, owner, initial_balance): self.owner = owner self.__balance = initial_balance # Private attribute (starts with __) def get_balance(self): return self.__balance def deposit(self, amount): if amount > 0: self.__balance += amount print(f"Deposited ${amount}. New balance: ${self.__balance}") else: print("Deposit amount must be positive!") def withdraw(self, amount): if amount > 0 and amount <= self.__balance: self.__balance -= amount print(f"Withdrew ${amount}. New balance: ${self.__balance}") else: print("Invalid withdrawal amount or insufficient funds!")# bank_account.py class BankAccount: def __init__(self, owner, initial_balance): self.owner = owner self.__balance = initial_balance # Private attribute (starts with __) def get_balance(self): return self.__balance def deposit(self, amount): if amount > 0: self.__balance += amount print(f"Deposited ${amount}. New balance: ${self.__balance}") else: print("Deposit amount must be positive!") def withdraw(self, amount): if amount > 0 and amount <= self.__balance: self.__balance -= amount print(f"Withdrew ${amount}. New balance: ${self.__balance}") else: print("Invalid withdrawal amount or insufficient funds!")
# main.py from bank_account import BankAccount account = BankAccount("Alice", 1000) # Can't directly access __balance # account.__balance = -500 # This won't work! # Must use the provided methods account.deposit(500) account.withdraw(200) print(f"Current balance: ${account.get_balance()}") # Invalid operations are prevented account.withdraw(2000) # Will show error message# main.py from bank_account import BankAccount account = BankAccount("Alice", 1000) # Can't directly access __balance # account.__balance = -500 # This won't work! # Must use the provided methods account.deposit(500) account.withdraw(200) print(f"Current balance: ${account.get_balance()}") # Invalid operations are prevented account.withdraw(2000) # Will show error message
Now:
- The balance is protected - can't be changed directly.
- All changes go through validated methods.
- Invalid operations are prevented automatically.
This approach makes the code:
- ✅ More secure - data can't be corrupted
- ✅ More reliable - validation ensures correct values
- ✅ Easier to maintain - all logic is in one place
🎁 Bonus: Public, Protected, and Private in Python
Python does not enforce strict access control like Java or C++.
Instead, it uses naming conventions to indicate intended access levels.
The goal is encapsulation, but Python trusts developers to follow conventions rather than forcing restrictions.
Public Attributes (No Underscore)
self.balance = 1000 # publicself.balance = 1000 # public
- Accessible from anywhere
- No protection
- Meant to be part of the class API
Protected Attributes (_single_underscore)
self._balance = 1000 # protected (by convention)self._balance = 1000 # protected (by convention)
- Indicates “internal use”
- Still fully accessible from outside
- Python does NOT enforce any restriction
Example:
account._balance = -500 # Allowed, but not recommendedaccount._balance = -500 # Allowed, but not recommended
Private Attributes (__double_underscore)
self.__balance = 1000 # private (name-mangled)self.__balance = 1000 # private (name-mangled)
Double underscore triggers name mangling:
self.__balance → self._BankAccount__balance
This hides the attribute but does not make it truly private.
You can still access it:
account._BankAccount__balance = -500 # Works!account._BankAccount__balance = -500 # Works!
Summary Table
| Level | Syntax | Accessible? | Enforced? | Notes |
|---|---|---|---|---|
| Public | balance | Everywhere | ❌ No | Part of API |
| Protected | _balance | Everywhere | ❌ No | Convention |
| Private | __balance | Mangled | ⚠️ Partial | Prevents accidental access |
Final Takeaway
Python focuses on intent, not strict enforcement.
Encapsulation is achieved through conventions, not hard restrictions.