Python Encapsulation
Encapsulation
Encapsulation is an object-oriented programming principle that bundles data and the methods that work on it into one unit — a class — and controls how that data can be accessed and changed.
Think of a bank account. You cannot reach into the bank's database and type a new balance. Instead, you use controlled operations — deposit, withdraw, check balance — and each one enforces the bank's rules. Encapsulation brings the same idea to your code.
In Python, encapsulation:
- Keeps data (variables) and behavior (methods) together inside a class
- Restricts direct access to sensitive or internal data
- Protects objects from accidental misuse, such as setting an invalid value
The result is code that is safer, easier to control, and easier to maintain.
The Problem Encapsulation Solves
Without encapsulation, any code can change an object's data to anything:
class Account:
def __init__(self, owner, balance):
self.owner = owner
self.balance = balance
acc = Account("Rahul", 10000)
acc.balance = -50000 # Nothing stops an invalid value
print(acc.balance)
Expected output:
-50000
The object is now in an invalid state. Encapsulation prevents this by hiding the data and allowing changes only through methods that check the rules.
Access Levels in Python
Python does not have keywords like private or public. Instead, it uses naming conventions with underscores:
| Name | Level | Meaning |
|---|---|---|
balance | Public | Free to use from anywhere |
_balance | Protected (by convention) | Intended for internal use; "please don't touch from outside" |
__balance | Private (name-mangled) | Python changes the name to make outside access harder |
Private Properties
To make a property private, start its name with two underscores (__).
Example: Creating a Private Property
class Account:
def __init__(self, owner, balance):
self.owner = owner
self.__balance = balance # Private property
acc = Account("Rahul", 10000)
print(acc.owner)
print(acc.__balance)
Output:
Rahul
AttributeError: 'Account' object has no attribute '__balance'
acc.owneris public, so it can be read normally.acc.__balancecannot be accessed directly from outside the class.
Note: Code inside the class can still use
self.__balancenormally. Only access from outside is blocked.
Accessing Private Data With a Getter Method
To allow reading a private value safely, provide a getter method.
Example: A Getter Method
class Account:
def __init__(self, owner, balance):
self.owner = owner
self.__balance = balance
def get_balance(self):
return self.__balance
acc = Account("Sneha", 15000)
print(acc.get_balance())
Expected output:
15000
The getter is defined inside the class, so it can read self.__balance and return it. Outside code can see the balance but cannot directly change it.
Modifying Private Data With a Setter Method
To allow changing private data in a controlled way, provide a setter method. A setter can validate a value before saving it.
Example: A Setter With Validation
class Account:
def __init__(self, owner, balance):
self.owner = owner
self.__balance = balance
def get_balance(self):
return self.__balance
def set_balance(self, amount):
if amount >= 0:
self.__balance = amount
else:
print("Balance cannot be negative")
acc = Account("Sneha", 15000)
print(acc.get_balance())
acc.set_balance(18000)
print(acc.get_balance())
acc.set_balance(-500)
print(acc.get_balance())
Expected output:
15000
18000
Balance cannot be negative
18000
What to notice:
- The valid value
18000is accepted and saved. - The invalid value
-500is rejected, and the balance stays18000. - Because the only way to change the balance is through
set_balance(), the rule "balance cannot be negative" is always enforced.
Tip: In real applications, raising an exception (
raise ValueError("Balance cannot be negative")) is often better than printing a message, because it forces the calling code to deal with the problem.
Why Encapsulation Is Important
- Data protection — internal data cannot be changed accidentally from outside.
- Validation — every change passes through methods that enforce the rules.
- Flexibility — the internal implementation can change (for example, storing the balance in paise instead of rupees) without affecting the code that uses the class, as long as the methods stay the same.
- Control — the class decides exactly how its data is read and modified.
Practical Example: Encapsulating Exam Marks
class ExamResult:
def __init__(self, student_name):
self.student_name = student_name
self.__marks = 0
def set_marks(self, marks):
if 0 <= marks <= 100:
self.__marks = marks
else:
print("Marks must be between 0 and 100")
def get_marks(self):
return self.__marks
def get_result(self):
return "Pass" if self.__marks >= 40 else "Fail"
result = ExamResult("Amit")
result.set_marks(78)
print(result.get_marks())
print(result.get_result())
result.set_marks(150)
print(result.get_marks())
Expected output:
78
Pass
Marks must be between 0 and 100
78
How the class protects its data:
__marksstarts at0and can only be changed throughset_marks().set_marks()accepts only values from 0 to 100, so150is rejected.get_result()calculates the result from the private marks. Outside code never needs to know how the pass mark is decided — if the rule changes, only this method changes.
Protected Properties
A single underscore (_) marks a property as protected. This is purely a convention that tells other developers: "this is for internal use — don't use it directly from outside the class."
Protected properties:
- Are meant for use inside the class and its subclasses
- Can still be accessed from outside, but should not be
Example: A Protected Property
class Employee:
def __init__(self, name, bonus):
self.name = name
self._bonus = bonus # Protected property
emp = Employee("Kiran", 5000)
print(emp.name)
print(emp._bonus) # Allowed, but discouraged
Expected output:
Kiran
5000
Note: Python does not enforce protected access. The single underscore is a signal to other programmers, not a rule the interpreter checks. In practice, the single underscore is the most common way Python developers mark internal attributes.
Private Methods
Methods can also be made private with a double underscore. Private methods are helper methods used inside the class, not meant to be called from outside.
Example: A Private Method
class MathTool:
def __init__(self):
self.total = 0
def __is_valid(self, value):
return isinstance(value, (int, float))
def add(self, value):
if self.__is_valid(value):
self.total += value
else:
print("Invalid input")
tool = MathTool()
tool.add(10)
tool.add(5)
tool.add("ten")
print(tool.total)
Expected output:
Invalid input
15
add()is public — it is the method outside code is meant to use.__is_valid()is private — an internal check thatadd()relies on."ten"is rejected because it is neither anintnor afloat.
Calling the private method from outside fails:
tool.__is_valid(5)
Output:
AttributeError: 'MathTool' object has no attribute '__is_valid'
Name Mangling
Python implements private names with a technique called name mangling. Inside a class, any name that starts with two underscores (and does not end with two underscores) is automatically renamed:
__value becomes _ClassName__value
This is why acc.__balance is "not found" from outside: the attribute's real name is _Account__balance.
Example: Seeing Name Mangling
class User:
def __init__(self, username, pin):
self.username = username
self.__pin = pin
user = User("admin", 4321)
print(user.__dict__)
print(user._User__pin) # Works, but not recommended
Expected output:
{'username': 'admin', '_User__pin': 4321}
4321
__dict__shows the object's real attribute names, including the mangled_User__pin.- Using the mangled name bypasses the protection — which shows that Python's privacy is not a security feature. It prevents accidental access and avoids name clashes in subclasses; it does not stop deliberate access.
Important: Never rely on private attributes to protect secrets such as passwords or PINs. Sensitive data needs real security measures, such as hashing and access control.
A Subtle Trap
Assigning to obj.__name from outside the class creates a brand-new, unrelated attribute — it does not change the private one:
acc = Account("Rahul", 10000)
acc.__balance = 0 # Creates a NEW attribute named '__balance'
print(acc.get_balance()) # The real private balance is unchanged
Expected output:
10000
Pythonic Encapsulation With @property
Writing get_balance() and set_balance() methods works, but Python offers a more natural approach: the @property decorator. It lets callers use attribute syntax while the class still runs validation code behind the scenes.
class Account:
def __init__(self, owner, balance):
self.owner = owner
self.__balance = balance
@property
def balance(self):
return self.__balance
@balance.setter
def balance(self, amount):
if amount < 0:
raise ValueError("Balance cannot be negative")
self.__balance = amount
acc = Account("Sneha", 15000)
print(acc.balance) # Calls the getter
acc.balance = 18000 # Calls the setter
print(acc.balance)
Expected output:
15000
18000
Setting acc.balance = -100 would raise ValueError: Balance cannot be negative. The calling code looks as simple as using a public attribute, but every change is validated.
Common Mistakes
| Mistake | Problem |
|---|---|
Expecting _name to block access | Single underscore is only a convention |
Treating __name as real security | Name mangling can be bypassed with _ClassName__name |
Assigning obj.__name = value from outside | Creates a new attribute; the private one is unchanged |
| Making everything private | Adds unnecessary getters and setters; protect only what needs rules |
| Setters that silently accept invalid data | Defeats the purpose of encapsulation — always validate |
Related Concepts
- Python Class Properties — instance and class attributes
- Python Class Methods — the public interface of a class
- Python Inheritance — how protected and private names behave in subclasses
- Python Functions — decorators such as
@property