Encapsulation bundles data (attributes) and the methods that operate on that data into a single unit (class), and restricts direct access to some components. It protects the internal state of an object from unintended modification.
Python uses naming conventions rather than strict keywords like private or protected.
| Convention | Example | Meaning |
|---|---|---|
| No prefix | self.name | Public - accessible anywhere |
Single underscore _ | self._salary | Protected - "don't touch from outside" (convention only) |
Double underscore __ | self.__password | Private - name-mangled, harder to access from outside |
class BankAccount:
def __init__(self, owner: str, balance: float):
self.owner = owner # public
self._account_type = "savings" # protected (convention)
self.__balance = balance # private (name-mangled)
def deposit(self, amount: float):
if amount > 0:
self.__balance += amount
def get_balance(self) -> float:
return self.__balance # controlled access
account = BankAccount("Alice", 1000)
print(account.owner) # Alice (public - fine)
print(account._account_type) # savings (works but discouraged)
print(account.get_balance()) # 1000 (via method - correct way)
# Direct access to __balance fails
# print(account.__balance) # AttributeError!
# Name mangling - Python renames it to _ClassName__attr
print(account._BankAccount__balance) # 1000 (possible but bad practice)
class Temperature:
def __init__(self, celsius: float = 0):
self._celsius = celsius
@property
def celsius(self) -> float:
"""Getter"""
return self._celsius
@celsius.setter
def celsius(self, value: float):
"""Setter with validation"""
if value < -273.15:
raise ValueError("Temperature below absolute zero!")
self._celsius = value
@property
def fahrenheit(self) -> float:
"""Computed property - no setter needed"""
return self._celsius * 9/5 + 32
@property
def kelvin(self) -> float:
return self._celsius + 273.15
t = Temperature(25)
print(t.celsius) # 25 - looks like attribute access
print(t.fahrenheit) # 77.0
print(t.kelvin) # 298.15
t.celsius = 100 # uses setter
print(t.fahrenheit) # 212.0
# t.celsius = -300 # ValueError!
class BankAccount:
def __init__(self, owner: str, initial_balance: float = 0):
self.__owner = owner
self.__balance = initial_balance
self.__transactions = []
@property
def owner(self) -> str:
return self.__owner
@property
def balance(self) -> float:
return self.__balance
def deposit(self, amount: float) -> None:
if amount <= 0:
raise ValueError("Deposit amount must be positive")
self.__balance += amount
self.__transactions.append(f"+${amount:.2f}")
def withdraw(self, amount: float) -> None:
if amount <= 0:
raise ValueError("Withdrawal amount must be positive")
if amount > self.__balance:
raise ValueError("Insufficient funds")
self.__balance -= amount
self.__transactions.append(f"-${amount:.2f}")
def get_statement(self) -> str:
history = "\n".join(self.__transactions) or "No transactions"
return f"Account: {self.__owner}\nBalance: ${self.__balance:.2f}\n{history}"
acc = BankAccount("Alice", 500)
acc.deposit(200)
acc.withdraw(100)
print(acc.get_statement())
# Account: Alice
# Balance: $600.00
# +$200.00
# -$100.00
Explore 500+ free tutorials across 20+ languages and frameworks.