Explain and apply the concepts of encapsulation and information hiding in OOP
Encapsulation is the process of bundling data (attributes) and methods (behaviour) together into a single unit — a class.
What is Encapsulation?
Encapsulation is the process of bundling data (attributes) and methods (behaviour) together into a single unit — a class.
┌─────────────────────────────────────────┐
│ BankAccount │
├─────────────────────────────────────────┤
│ – account_number │
│ – name │
│ – balance │
├─────────────────────────────────────────┤
│ + deposit() │
│ + withdraw() │
│ + get_balance() │
└─────────────────────────────────────────┘
↑ Data + Methods in ONE class
The key principle: data cannot be accessed directly from outside the class — it is protected inside.
What is Information Hiding?
Information hiding is the result achieved through encapsulation: the internal details of how an object works are hidden from the outside world.
| Concept | Description |
|---|---|
| Encapsulation | The process of bundling data + methods into a class |
| Information Hiding | The result — data is protected and not directly accessible from outside |
- The internal representation of an object is hidden from view outside the class
- Only a controlled interface (public methods) is exposed
- Internal details can change without breaking code that uses the class
Encapsulation vs Information Hiding
| Encapsulation | Information Hiding | |
|---|---|---|
| Nature | Process | Result |
| Focus | Bundling data + methods together | What is visible vs hidden from outside |
| Analogy | Making the capsule | What is inside the capsule |
| Can one exist without the other? | Information hiding cannot happen without encapsulation ✅ | No ❌ |
Implementation in Python
By default, all attributes and methods in Python are public. To restrict access, use naming conventions:
| Convention | Meaning | Visibility |
|---|---|---|
attribute | Public | Accessible from anywhere |
_attribute | Protected | Intended for internal use (soft convention) |
__attribute | Private | Name mangling makes external access very difficult |
Name mangling: Python renames __attribute to _ClassName__attribute internally, making accidental access harder.
class Example:
def __init__(self):
self.public = "anyone can see"
self._protected = "internal use only"
self.__private = "hidden from outside"
obj = Example()
print(obj.public) # anyone can see ✅
print(obj._protected) # internal use only ⚠️ works but not recommended
print(obj.__private) # AttributeError ❌
print(obj._Example__private) # hidden from outside ⚠️ technically works
Getters and Setters
To access or modify private attributes, you must go through controlled public methods:
| Method type | Purpose | Convention |
|---|---|---|
| Getter | Retrieve a value without modifying it | get_attribute() |
| Setter | Modify a value with validation | set_attribute(new_value) |
Why use getters and setters?
- Controls how data is accessed and modified
- Allows validation before changing a value
- Hides internal representation from outside code
- Enables future changes to the class without breaking external code
Bank Account Example
class BankAccount:
"""Encapsulates a bank account with private data and controlled access."""
def __init__(self, account_number: str, name: str, balance: float):
self.__account_number = account_number # Private
self.__name = name # Private
self.__balance = 0 # Private
if balance > 0: # Validation in constructor
self.__balance = balance
# ── Getters ──────────────────────────────────
def get_balance(self) -> float: # Getter for balance
return self.__balance
def get_account_number(self) -> str: # Getter for account number
return self.__account_number
# ── Setters ─────────────────────────────────
def set_balance(self, new_balance: float): # Setter for balance
if new_balance >= 0: # Validation: balance cannot be negative
self.__balance = new_balance
def set_name(self, new_name: str): # Setter for name
if new_name != "":
self.__name = new_name
# ── Business methods ─────────────────────────
def deposit(self, amount: float) -> bool:
if amount > 0:
self.__balance += amount
return True
return False
def withdraw(self, amount: float) -> bool:
# Validation: amount must be positive and within balance
if amount > 0 and amount <= self.__balance:
self.__balance -= amount
return True
return False
account = BankAccount("ACC001", "Alice", 1000.00)
print(account.get_balance()) # 1000.0
account.deposit(500)
print(account.get_balance()) # 1500.0
account.withdraw(200)
print(account.get_balance()) # 1300.0
account.set_balance(-500) # Ignored — balance cannot be negative
print(account.get_balance()) # 1300.0 ✅
# account.__balance = 99999 # AttributeError ❌ Direct access blocked
What Happens Without Encapsulation?
Without encapsulation, data is exposed and can be modified without any control:
# ❌ BAD — no encapsulation
class BankAccount:
def __init__(self, balance: float):
self.balance = balance # Public — no protection
account = BankAccount(1000)
account.balance = -9999999 # Nothing stops this!
# ✅ GOOD — encapsulation with setter
class BankAccount:
def __init__(self, balance: float):
self.__balance = 0 # Private
if balance > 0:
self.__balance = balance
def set_balance(self, new_balance: float):
if new_balance >= 0: # Validation blocks invalid values
self.__balance = new_balance
account = BankAccount(1000)
account.set_balance(-9999999) # Ignored — validation prevents it
Advantages of Encapsulation
| Advantage | Explanation |
|---|---|
| Data protection | Private data cannot be changed directly — only through validated methods |
| Controlled modification | Setters can enforce rules (e.g., balance cannot be negative) |
| Reduced coupling | External code depends only on the public interface — internal changes don’t break it |
| Simplicity | Users of a class only need to understand the public interface, not internal complexity |
| Maintainability | Changing the internal implementation (e.g., renaming a private variable) requires changes only inside the class |
| Reusability | Well-encapsulated classes can be reused in different programs without modification |