Python Encapsulation & Properties
Encapsulation is the bundling of data (attributes) and the methods that operate on that data into a single unit, while restricting direct outside access to internal implementation details.
In Python, access modifiers are governed by naming conventions rather than strict language keywords (like private in Java/C++):
| Modifier | Naming Syntax | Accessibility / Convention |
|---|---|---|
| Public | self.name | Accessible freely from anywhere (inside and outside the class). |
| Protected | self._balance | Convention only: Signals to other developers that this is internal and should only be accessed by this class and its subclasses. |
| Private | self.__pin | Enforced by Python: Triggers Name Mangling to prevent accidental outside access or subclass overriding. |
class UserAccount:
def __init__(self, username, email, pin):
self.username = username # Public (Freely accessible)
self._email = email # Protected (Convention: internal use)
self.__pin = pin # Private (Name mangled!)
def verify_pin(self, entered_pin):
return self.__pin == entered_pin
user = UserAccount("balaji_dev", "balaji@test.com", 1234)
# 1. Accessing Public attribute:
print("Public Username:", user.username)
# 2. Accessing Protected attribute (Works, but discouraged by convention):
print("Protected Email:", user._email)
# 3. Attempting to access Private attribute directly raises AttributeError:
try:
print(user.__pin)
except AttributeError as err:
print("โ Private Access Blocked:", err)
Python actively protects private variables by rewriting their names in memory so external callers cannot accidentally read or corrupt them.
When Python encounters an identifier starting with two or more leading underscores (__pin), it automatically rewrites the variable name internally to _ClassName__attribute (e.g. _UserAccount__pin).
This mechanism is known as Name Mangling. Its primary purpose is not cryptography, but to prevent accidental name collisions in inheritance hierarchies when subclasses define attributes with identical names.
# Inspecting the internal __dict__ table of the instance:
user = UserAccount("ravi_k", "ravi@test.com", 9988)
print("Instance Memory Dictionary (__dict__):")
print(user.__dict__)
# Accessing the mangled name directly (Proof of Name Mangling):
print("\nAccessing via mangled name (_UserAccount__pin):", user._UserAccount__pin)
As Python core developers famously say, "We are all consenting adults here." Name mangling discourages unsafe access while still leaving the door open for debugging tools and serializers.
In languages like Java, developers are forced to write verbose boilerplate methods: getBalance() and setBalance(val). In Python, you can expose methods as if they were simple attributes using the @property decorator!
This allows you to add validation, type checking, and computed logic transparently without breaking existing code that accesses the attribute directly:
class BankAccount:
def __init__(self, owner, initial_balance):
self.owner = owner
self._balance = 0.0 # Protected backing field
self.balance = initial_balance # Routes through @balance.setter for validation!
# 1. Getter property:
@property
def balance(self):
"""Getter: returns the current balance."""
return self._balance
# 2. Setter property with validation:
@balance.setter
def balance(self, value):
"""Setter: enforces positive balance rule."""
if not isinstance(value, (int, float)):
raise TypeError("Balance must be a numeric value!")
if value < 0:
raise ValueError(f"Balance cannot be negative! Attempted: โน{value}")
self._balance = float(value)
# 3. Read-Only computed property:
@property
def formatted_balance(self):
return f"โน{self._balance:,.2f}"
# Test the property decorator:
acc = BankAccount("Balaji", 5000)
print("Balance accessed as attribute:", acc.balance) # Calls getter!
print("Formatted:", acc.formatted_balance)
# Update balance cleanly (Calls setter validation):
acc.balance = 7500
print("Updated Balance:", acc.formatted_balance)
# Attempt invalid negative update:
try:
acc.balance = -1000 # Triggers ValueError!
except ValueError as err:
print("๐ซ Validation Prevented Error:", err)
Callers use clean syntax acc.balance = 7500 without ugly method calls like acc.setBalance(7500), while your class retains 100% control over validation.
Inside the setter "def balance(self, value):", writing "self.balance = value" calls the setter again endlessly until RecursionError crashes Python! You must assign to the private backing variable: self._balance = value.
Create a Temperature class with a celsius property. Add a getter and setter with a rule that temperature cannot be below absolute zero (-273.15ยฐC).
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError("Temperature below absolute zero is impossible!")
self._celsius = value
t = Temperature(25)
print(f"Temperature: {t.celsius}ยฐC")
t.celsius = 38
print(f"Updated: {t.celsius}ยฐC")
Q Is there true private data in Python like private in Java/C++?
No. Python uses name mangling (_ClassName__var) to protect private attributes. It prevents accidental collisions and warns developers, but does not provide hard memory-level access barriers.
Q What is the @deleter decorator in Python properties?
@property_name.deleter allows you to customize what happens when someone executes "del obj.property_name", such as resetting a cached value or cleaning up related resources.
Q Can a property be read-only in Python?
Yes! Simply define the @property getter without defining a corresponding @setter. Any attempt to assign to the attribute will raise an AttributeError: property has no setter.