Unit 5 · Advanced Topics
Encapsulation
Learn about encapsulation in Python — data hiding, access modifiers (public, protected, private), name mangling, getters and setters, the @property decorator, and real-world applications.
Table of Contents
What is Encapsulation?
University Definition
Encapsulation is one of the four pillars of OOP that bundles data (attributes) and methods that operate on that data into a single unit (class) and restricts direct access to some of the object's components. This protects the internal state of the object and prevents external code from putting it into an invalid or inconsistent state.
Encapsulation in Real Life: ============================ Car Example: - You use steering wheel (public interface) - You don't access engine internals directly - Engine is "encapsulated" inside the car ATM Example: - You insert card and enter PIN (interface) - You don't access bank database directly - Internal workings are hidden Why encapsulate? 1. Data protection (prevent invalid states) 2. Flexibility (change internal implementation) 3. Debugging (errors traced to specific methods) 4. Security (hide sensitive data)
Data Hiding
University Definition
Data hiding is the practice of restricting direct access to an object's internal data and requiring all interactions to go through well-defined methods (getters and setters). This ensures that data is accessed and modified only in controlled ways.
# WITHOUT encapsulation - data is exposed
class AccountBad:
def __init__(self, balance):
self.balance = balance # Anyone can modify!
acc = AccountBad(1000)
acc.balance = -5000 # Invalid! No protection
print(acc.balance) # -5000 (dangerous!)
# WITH encapsulation - data is protected
class AccountGood:
def __init__(self, balance):
self.__balance = balance # Private!
def get_balance(self):
return self.__balance
def deposit(self, amount):
if amount > 0:
self.__balance += amount
else:
print("Invalid deposit amount!")
def withdraw(self, amount):
if 0 < amount <= self.__balance:
self.__balance -= amount
else:
print("Invalid withdrawal!")
acc2 = AccountGood(1000)
acc2.deposit(500)
print(acc2.get_balance()) # 1500
acc2.withdraw(300)
print(acc2.get_balance()) # 1200
# acc2.__balance # AttributeError - hidden!
Access Modifiers
class Employee:
def __init__(self, name, salary, ssn):
self.name = name # PUBLIC - accessible anywhere
self._department = "IT" # PROTECTED - convention only
self.__ssn = ssn # PRIVATE - name mangled
def display(self):
print(f"Name: {self.name}")
print(f"Department: {self._department}")
print(f"SSN: {self.__ssn}")
emp = Employee("Rahul", 50000, "123-45-6789")
# PUBLIC - accessible everywhere
print(emp.name) # Rahul
emp.name = "Priya"
print(emp.name) # Priya
# PROTECTED - accessible but convention says don't
print(emp._department) # IT (works but shouldn't access)
emp._department = "HR" # Works but bad practice
# PRIVATE - not directly accessible
# print(emp.__ssn) # AttributeError!
# emp.__ssn = "000" # AttributeError!
# Accessing private via name mangling
print(emp._Employee__ssn) # 123-45-6789 (works but bad!)
emp._Employee__ssn = "000" # Works but bad practice!
| Modifier | Syntax | Inside Class | Outside Class | In Child Class |
|---|---|---|---|---|
| Public | self.name | Yes | Yes | Yes |
| Protected | self._name | Yes | Yes (by convention, don't) | Yes |
| Private | self.__name | Yes | No (name mangled) | No (name mangled) |
Name Mangling
University Definition
Name mangling is a Python mechanism where identifiers prefixed with double underscores (__name) are internally transformed to _ClassName__name. This prevents accidental access from outside the class and avoids name conflicts in inheritance.
class MyClass:
def __init__(self):
self.public_var = "public"
self._protected_var = "protected"
self.__private_var = "private"
obj = MyClass()
# Show name mangling
print(obj.public_var) # public
print(obj._protected_var) # protected
# print(obj.__private_var) # AttributeError!
# Python transforms __private_var to _MyClass__private_var
print(obj._MyClass__private_var) # private (name mangled)
# Check what attributes exist
print(dir(obj)) # Shows _MyClass__private_var
# Name mangling in inheritance
class Parent:
def __init__(self):
self.__secret = "parent secret"
class Child(Parent):
def get_secret(self):
# return self.__secret # AttributeError!
return self._Parent__secret # Access via mangled name
child = Child()
print(child.get_secret()) # parent secret
# No conflict - child can have its own __secret
class Child2(Parent):
def __init__(self):
super().__init__()
self.__secret = "child secret" # Different from parent's!
c = Child2()
print(c._Child2__secret) # child secret
print(c._Parent__secret) # parent secret
Getters and Setters
class Student:
def __init__(self, name, marks):
self.__name = name
self.__marks = marks
# Getter method
def get_name(self):
return self.__name
def get_marks(self):
return self.__marks
# Setter method with validation
def set_name(self, name):
if isinstance(name, str) and len(name) > 0:
self.__name = name
else:
print("Invalid name!")
def set_marks(self, marks):
if 0 <= marks <= 100:
self.__marks = marks
else:
print("Marks must be between 0 and 100!")
def get_grade(self):
if self.__marks >= 90: return "A+"
elif self.__marks >= 80: return "A"
elif self.__marks >= 70: return "B"
elif self.__marks >= 60: return "C"
else: return "F"
# Using getters and setters
s = Student("Rahul", 85)
print(f"Name: {s.get_name()}") # Name: Rahul
print(f"Marks: {s.get_marks()}") # Marks: 85
print(f"Grade: {s.get_grade()}") # Grade: A
s.set_marks(92)
print(f"Updated marks: {s.get_marks()}") # 92
s.set_marks(150) # Marks must be between 0 and 100!
s.set_name("") # Invalid name!
print(f"Name: {s.get_name()}") # Still Rahul
@property Decorator
University Definition
The @property decorator allows you to define methods that can be accessed like attributes, providing a Pythonic way to implement getters and setters. It maintains the interface while adding validation internally.
class Temperature:
def __init__(self, celsius=0):
self.__celsius = celsius
@property
def celsius(self):
"""Getter - accessed like an attribute"""
return self.__celsius
@celsius.setter
def celsius(self, value):
"""Setter - called when assigning"""
if value < -273.15:
raise ValueError("Temperature below absolute zero!")
self.__celsius = value
@property
def fahrenheit(self):
"""Computed property (read-only)"""
return (self.__celsius * 9/5) + 32
@fahrenheit.setter
def fahrenheit(self, value):
"""Allow setting in Fahrenheit"""
self.__celsius = (value - 32) * 5/9
# Using property - looks like attribute access!
t = Temperature(25)
print(f"Celsius: {t.celsius}") # 25
print(f"Fahrenheit: {t.fahrenheit}") # 77.0
t.celsius = 100 # Uses setter
print(f"Boiling point: {t.fahrenheit}") # 212.0
t.fahrenheit = 32 # Uses fahrenheit setter
print(f"Freezing: {t.celsius}") # 0.0
# t.celsius = -300 # ValueError!
class Circle:
def __init__(self, radius):
self.radius = radius # Uses the setter!
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
if value < 0:
raise ValueError("Radius cannot be negative")
self._radius = value
@property
def area(self):
return 3.14159 * self._radius ** 2
@property
def circumference(self):
return 2 * 3.14159 * self._radius
c = Circle(5)
print(f"Radius: {c.radius}") # 5
print(f"Area: {c.area}") # 78.53975
print(f"Circumference: {c.circumference}") # 31.4159
c.radius = 10
print(f"New area: {c.area}") # 314.159
Output:
Celsius: 25 Fahrenheit: 77.0 Boiling point: 212.0 Freezing: 0.0 Radius: 5 Area: 78.53975 Circumference: 31.4159 New area: 314.159
Real-World Example: Bank Account
class BankAccount:
def __init__(self, owner, balance=0):
self.__owner = owner
self.__balance = balance
self.__transactions = []
@property
def owner(self):
return self.__owner
@property
def balance(self):
return self.__balance
def deposit(self, amount):
if amount <= 0:
print("Invalid deposit amount!")
return
self.__balance += amount
self.__transactions.append(f"+{amount}")
print(f"Deposited: {amount}. Balance: {self.__balance}")
def withdraw(self, amount):
if amount <= 0:
print("Invalid withdrawal amount!")
return
if amount > self.__balance:
print("Insufficient funds!")
return
self.__balance -= amount
self.__transactions.append(f"-{amount}")
print(f"Withdrawn: {amount}. Balance: {self.__balance}")
def get_statement(self):
print(f"\n{'='*40}")
print(f"Account: {self.__owner}")
print(f"Transactions: {', '.join(self.__transactions)}")
print(f"Final Balance: {self.__balance}")
print(f"{'='*40}")
# Usage
acc = BankAccount("Rahul", 10000)
print(f"Owner: {acc.owner}") # Property access
print(f"Balance: {acc.balance}") # Property access
acc.deposit(5000) # Deposited: 5000. Balance: 15000
acc.withdraw(3000) # Withdrawn: 3000. Balance: 12000
acc.withdraw(20000) # Insufficient funds!
acc.deposit(-100) # Invalid deposit amount!
acc.get_statement()
# Cannot access private data directly
# acc.__balance = 9999999 # AttributeError!
# acc.__balance # AttributeError!
Output:
Owner: Rahul Balance: 10000 Deposited: 5000. Balance: 15000 Withdrawn: 3000. Balance: 12000 Insufficient funds! Invalid deposit amount! ======================================== Account: Rahul Transactions: +5000, -3000 Final Balance: 12000 ========================================
Common Mistakes to Avoid
- Using name mangling (
_Class__var) to access private attributes — defeats the purpose. - Confusing protected (
_var) with private (__var) — protected is convention only. - Making everything private — over-encapsulation reduces usability.
- Not using @property when attribute access syntax is preferred.
- Forgetting that encapsulation in Python is by convention, not enforced by the language.
Practice Questions
- Create a
Personclass with private age attribute and a setter that rejects negative ages. - Implement a
Walletclass with encapsulated balance, only allowing deposits and withdrawals through methods. - Use the @property decorator to create a
Rectangleclass where area and perimeter are computed properties. - Demonstrate name mangling by creating a parent class with a private variable and a child class that tries to access it.
Key Points
Encapsulation bundles data and methods, restricting direct access to internal state.
Public (var) — accessible everywhere; Protected (_var) — convention only; Private (__var) — name mangled.
Name mangling transforms __var to _Class__var to prevent accidental access.
Getters and setters provide controlled access to private attributes with validation.
The @property decorator provides a Pythonic way to implement getters/setters with attribute syntax.
Encapsulation protects data integrity and makes code more maintainable and secure.