Encapsulation means keeping object data behind clear methods or properties instead of changing it freely everywhere.
Python uses naming conventions and properties to signal which values are public and which details should stay internal.
The goal is not hiding code from other developers. The goal is protecting rules, validation, and object consistency.
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:
"""Return the stored Celsius value."""
return self._celsius
@celsius.setter
def celsius(self, value: float) -> None:
"""Store a physically valid Celsius value."""
if value < -273.15:
raise ValueError("temperature is below absolute zero")
self._celsius = value
@property
def fahrenheit(self) -> float:
return self._celsius * 9 / 5 + 32
temperature = Temperature(25)
print(temperature.celsius)
print(temperature.fahrenheit)
25
77.0
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
Try this next
0 of 3 completed
Python uses conventions such as _name and name mangling with __name, but it relies on developer discipline more than strict privacy.
Use property when reading an attribute should look simple, but setting it needs validation or computed behavior.
Direct changes can skip validation. Methods and properties keep the object in a valid state.
Explore 500+ free tutorials across 20+ languages and frameworks.