Phase: 5. OOP | Estimated time: 1.5 hours | Milestone Project: No
- Module 044 (Class vs Instance Variables)
By the end of this module, you will be able to:
- Explain the concept of encapsulation
- Use
_protectednaming convention - Use
__namemangling for private-like attributes - Implement getter/setter patterns
- Understand why encapsulation matters for maintainable code
Encapsulation hides internal details and exposes only what's necessary. This prevents accidental misuse and makes it safe to change internals without breaking external code.
Encapsulation means bundling data with the methods that operate on it and restricting direct access to internal state.
┌──────────────────────────────┐
│ Object │
│ │
│ ╔══════════╗ public API │
│ ║ data ║ ←────────── │
│ ║ ║ methods │
│ ╚══════════╝ │
│ ▲ internal state │
│ │ (hidden) │
└───────┴──────────────────────┘
Python uses conventions rather than strict access control. Everything is public — trust is assumed.
| Convention | Syntax | Meaning |
|---|---|---|
| Public | self.name |
Part of the public API |
| Protected | self._name |
Internal use (convention only) |
| Name mangled | self.__name |
Stronger hint of privacy |
class BankAccount:
"""A bank account with encapsulation."""
def __init__(self, owner, balance):
self.owner = owner # public
self._branch_code = "001" # protected — internal use
self.__balance = balance # name mangled — "private"
def deposit(self, amount):
"""Deposit money."""
if amount > 0:
self.__balance += amount
def get_balance(self):
"""Get current balance."""
return self.__balanceWhen you prefix with __, Python renames it: _ClassName__attr. This avoids accidental overrides in subclasses but is still accessible if you know the mangled name.
acc = BankAccount("Alice", 1000)
# print(acc.__balance) # AttributeError!
print(acc._BankAccount__balance) # 1000 — still accessible but clearly internalBefore Python's @property (covered fully in Module 052), you can use explicit getter/setter methods:
class Temperature:
"""Temperature with getter/setter."""
def __init__(self, celsius=0):
self._celsius = celsius
def get_celsius(self):
"""Return temperature in Celsius."""
return self._celsius
def set_celsius(self, value):
"""Set temperature, ensuring sensible range."""
if value < -273.15:
raise ValueError("Temperature below absolute zero!")
self._celsius = value
def get_fahrenheit(self):
"""Convert to Fahrenheit."""
return self._celsius * 9/5 + 32t = Temperature(25)
print(t.get_celsius()) # 25
print(t.get_fahrenheit()) # 77.0
t.set_celsius(30)- Validation: Prevent invalid state (e.g., negative age)
- Decoupling: Change internal implementation without affecting users
- Debugging: Add logging or breakpoints easily at the method level
- Contracts: Methods document what goes in/out; direct attribute access bypasses them
- Thinking
__makes truly private: It's still accessible via_ClassName__attr. - Over-encapsulating: Not everything needs a getter — plain attributes are fine when no logic is required.
- Confusing
_and__: Use_for "internal use" and__to avoid name collisions in inheritance.
- Create a
class Personwith a "private"__ageattribute. - Add getter
get_age()and setterset_age(age)with validation (age > 0 and < 150). - Attempt to access
__agedirectly and via the mangled name. - Create a subclass and see how name mangling prevents accidental overrides.
- Encapsulation hides internal state and exposes a controlled interface.
_name= protected convention;__name= name mangling for stronger privacy.- Getter/setter methods allow validation and future flexibility.
- Encapsulation makes code more robust and maintainable.
Continue to Module 046: Inheritance.