Chapter 2: Encapsulation & Data Hiding
2.1 Data Hiding and Access Modifiers – Public, Private, and Protected Members
What is Encapsulation?
Encapsulation is one of the fundamental principles of Object-Oriented Programming (OOP).
It is the process of wrapping the data (variables) and code acting on the data (methods)
together as a single unit. This concept is often compared to a capsule in the medical field,
where all the components are wrapped inside one container.
Benefits of Encapsulation:
1. Control over data: Encapsulation allows a class to control the accessibility and
modification of its fields.
2. Increased security: It hides the internal object details from the outside world.
3. Easier maintenance and flexibility: Changes to encapsulated code can be made with
minimal impact on other parts of the codebase.
4. Code reusability and modularity: Encapsulation encourages writing self-contained,
reusable code blocks.
Data Hiding in Python
Data Hiding refers to restricting access to the internal data or functionality of an object. This
is a key part of encapsulation. Python doesn’t have built-in access modifiers like other
languages (e.g., private, protected in Java/C++), but it achieves data hiding through naming
conventions and mechanisms like name mangling.
Python achieves data hiding primarily through name mangling and convention-based access
modifiers.
Access Modifiers in Python
Python has three types of access modifiers to restrict the access of class members:
Modifier Type Syntax Example Accessibility
Public self.name Accessible from anywhere
Protected self._name Should not be accessed outside class or subclass
Private self.__name Name mangling is applied, access limited
1. Public Members
• Members (variables or methods) declared without any underscore prefix.
• These are accessible anywhere, both within and outside the class.
class Car:
def __init__(self, brand):
self.brand = brand # public member
c = Car("Toyota")
print(c.brand) # Output: Toyota
Usage:
• Use public members when you want the attributes or methods to be freely
accessible and modifiable.
2. Protected Members
• Prefixed with a single underscore: _member.
• Indicates that the member is intended to be protected, i.e., used only within the
class and its subclasses.
• Not enforced by Python — it's a convention.
class Vehicle:
def __init__(self, brand):
self._brand = brand # protected member
class Car(Vehicle):
def display(self):
print(f"Brand: {self._brand}")
c = Car("Honda")
c.display() # Accessing protected member
print(c._brand) # Technically allowed but discouraged
Usage:
• Used when you want to restrict access, but still allow subclasses to access or override
the member.
3. Private Members
• Prefixed with double underscores: __member.
• Python uses name mangling to make these members less accessible.
class Secret:
def __init__(self):
self.__code = 1234 # private member
s = Secret()
# print(s.__code) # Raises AttributeError
print(s._Secret__code) # Access using name mangling
What is Name Mangling?
• Python internally changes the name of private variables by prefixing it with
_ClassName.
• This helps avoid accidental modification or access.
Usage:
• Use when you want to hide data and prevent subclass from modifying it.
Summary Table: Access Modifiers
Modifier Prefix Accessible in Class Accessible in Subclass Accessible Outside
Public None Yes Yes Yes
Protected -------- Yes Yes Not Recommended
Private ------- Yes No (via mangling only) No (via mangling)
2.2 Getters and Setters – Using Property Decorators (@property)
Getter:
A getter is a method used to access or retrieve the value of a private or protected attribute
of a class. It provides a controlled way to read the value without directly accessing the
variable.
Definition:
A getter is a method that gets (returns) the value of an instance variable, typically one that
is private (__variable) or protected (_variable).
Setter:
A setter is a method used to set or update the value of a private or protected attribute. It
often includes validation logic to ensure that the data being assigned is appropriate.
Definition:
A setter is a method that sets (modifies) the value of an instance variable, ensuring proper
encapsulation and validation.
Why use Getters and Setters?
• Direct access to class attributes breaks encapsulation.
• Getters and Setters control access to private data.
• They allow validation, logging, or triggers during getting/setting values.
Traditional Getters and Setters in Python
class Person:
def __init__(self, age):
self.__age = age
def get_age(self):
return self.__age
def set_age(self, value):
if value > 0:
self.__age = value
else:
raise ValueError("Age must be positive")
p = Person(25)
print(p.get_age()) # 25
p.set_age(30)
print(p.get_age()) # 30
Using @property in Python (Pythonic Way)
Python provides a more elegant solution via the @property decorator, allowing access to
private variables as if they were public.
class Person:
def __init__(self, age):
self.__age = age
@property
def age(self):
return self.__age
@age.setter
def age(self, value):
if value > 0:
self.__age = value
else:
raise ValueError("Age must be positive")
p = Person(28)
print(p.age) # calls getter
p.age = 32 # calls setter
Advantages of @property Decorator
1. Cleaner, Pythonic syntax
2. Enables read-only, write-only, or read-write attributes
3. You can modify logic without changing external interface
Read-only Property Example
class Product:
def __init__(self, name):
self.__name = name
@property
def name(self):
return self.__name
p = Product("Laptop")
print(p.name) # Allowed
# p.name = "Tablet" # Error! No setter
Write-only Property Example
class SecretBox:
def __init__(self):
self.__secret = None
@property
def secret(self):
raise AttributeError("Not allowed to read secret")
@secret.setter
def secret(self, value):
self.__secret = value
box = SecretBox()
box.secret = "TopSecret" # Allowed
# print(box.secret) # Error
Best Practices with Getters and Setters
• Avoid unnecessary use unless control is required.
• Don’t overuse property decorators – use them when there's logic to protect/validate.
• Combine with private members to enforce encapsulation.
2.3 Name Mangling in Python
What is Name Mangling?
Name mangling is a mechanism in Python to make private variables harder to access from
outside the class by changing the name internally.
Python changes the name of private variables to _ClassName__variable.
Why Name Mangling?
• Prevent accidental overrides in subclasses.
• Protect data integrity.
Example of Name Mangling
class BankAccount:
def __init__(self, balance):
self.__balance = balance # private
def get_balance(self):
return self.__balance
a = BankAccount(1000)
# print(a.__balance) # AttributeError
print(a._BankAccount__balance) # 1000
Python doesn’t completely hide data — it uses this mechanism to discourage external
access.
Use Case: Avoiding Attribute Clashes in Inheritance
class Parent:
def __init__(self):
self.__data = 10
class Child(Parent):
def __init__(self):
super().__init__()
self.__data = 20
c = Child()
print(c._Parent__data) # 10
print(c._Child__data) # 20
Without mangling, both __data would point to the same variable — which is not what we
want in this case.
Encapsulation vs Abstraction
Encapsulation Abstraction
Hides internal state/data Hides implementation details
Done through access modifiers Done through abstract classes/interfaces
Achieved via class mechanisms Achieved via inheritance/polymorphism