python › Encapsulation in Python
phoolchandra · Oct 23, 2026 · Published
Would you like anyone to directly change your bank balance to zero with one line of code? Of course not! Encapsulation is the OOP principle that protects an object's data and allows changes only through safe, controlled methods. In this blog you will learn public, protected and private members, name mangling, getters and setters, and the Pythonic @property decorator.
Watch the video first, then use this blog as your notes. Every section has runnable examples, and at the end you will find a "Predict the Output" quiz, practice challenges, a self-check list, interview questions and a quick summary.
How to use this blog: Open the Python shell (type python in your terminal) or any online Python editor and type every example yourself. Change the values, break the code and see what happens — that is how concepts really stick.
Encapsulation means bundling data (attributes) and the methods that work on that data into a single unit (a class), and restricting direct access to some of that data to protect its integrity.
Encapsulation vs Abstraction: Encapsulation is about hiding and protecting data (how access is controlled). Abstraction is about hiding complexity (showing only what is necessary). They work together.
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner
self.balance = balance
acc = BankAccount("Riya", 1000)
acc.balance = -50000 # anyone can set ANY value - no validation!
print(acc.balance) # -50000Python does not have public / private keywords like Java or C++. Instead it uses naming conventions with underscores.
| Type | Syntax | Accessible from | Meaning |
|---|---|---|---|
| Public | self.name | Anywhere | Normal attribute — free to use |
| Protected | self._name | Class and subclasses (by convention) | "Internal use — please don't touch from outside" |
| Private | self.__name | Only inside the class | Name-mangled to prevent accidental access |
class Employee:
def __init__(self, name, department, salary):
self.name = name # public
self._department = department # protected
self.__salary = salary # private
def show(self):
# all three are accessible INSIDE the class
print(self.name, self._department, self.__salary)
e = Employee("Aman", "IT", 50000)
e.show() # Aman IT 50000
print(e.name) # Aman
print(e._department) # IT (works, but you SHOULDN'T do this)
print(e.__salary) # AttributeError: 'Employee' object has no attribute '__salary'When you write self.__salary, Python internally renames it to self._Employee__salary. This is called name mangling.
print(e._Employee__salary) # 50000 - still reachable!
print([a for a in dir(e) if "salary" in a]) # ['_Employee__salary']Python philosophy: "We are all consenting adults here." Python doesn't make data impossible to access — it makes it clear that you shouldn't. Name mangling mainly prevents accidental overriding of attributes in subclasses.
class PaymentGateway:
def pay(self, amount):
if self.__verify():
print(f"Paid Rs {amount}")
def __verify(self): # private helper method
print("Verifying user...")
return True
p = PaymentGateway()
p.pay(500)
# Verifying user...
# Paid Rs 500
# p.__verify() -> AttributeErrorclass BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self.__balance = balance
def get_balance(self): # getter
return self.__balance
def deposit(self, amount): # controlled setter
if amount <= 0:
print("Deposit amount must be positive!")
return
self.__balance += amount
def withdraw(self, amount):
if amount > self.__balance:
print("Insufficient balance!")
else:
self.__balance -= amount
acc = BankAccount("Riya", 1000)
acc.deposit(-500) # Deposit amount must be positive!
acc.withdraw(5000) # Insufficient balance!
acc.deposit(500)
print(acc.get_balance()) # 1500Now the balance can only change through methods that validate the data. That is encapsulation in action.
Getters and setters work, but obj.get_x() / obj.set_x(v) is Java style. In Python, use @property — it lets you use simple attribute syntax with validation behind the scenes.
class Student:
def __init__(self, name, marks):
self.name = name
self.marks = marks # this calls the setter below!
@property
def marks(self): # getter
return self._marks
@marks.setter
def marks(self, value): # setter with validation
if not 0 <= value <= 100:
raise ValueError("Marks must be between 0 and 100")
self._marks = value
s = Student("Riya", 92)
print(s.marks) # 92 (looks like a normal attribute)
s.marks = 95 # setter runs automatically
print(s.marks) # 95
s.marks = 150 # ValueError: Marks must be between 0 and 100class Circle:
def __init__(self, radius):
self.radius = radius
@property
def area(self): # computed, read-only
return round(3.14159 * self.radius ** 2, 2)
c = Circle(2)
print(c.area) # 12.57
# c.area = 50 -> AttributeError (no setter defined)class Vehicle:
def __init__(self):
self._speed = 0 # protected - meant for subclasses
self.__engine_no = "E123" # private - mangled to _Vehicle__engine_no
class Car(Vehicle):
def accelerate(self):
self._speed += 20 # OK - subclasses may use protected members
print("Speed:", self._speed)
def engine(self):
print(self.__engine_no) # AttributeError! looks for _Car__engine_no
c = Car()
c.accelerate() # Speed: 20class UserAccount:
def __init__(self, username, password):
self.username = username
self.__password = None
self.set_password(password)
def set_password(self, new_password):
if len(new_password) < 8:
print("Password must be at least 8 characters!")
return
self.__password = new_password
print("Password set successfully")
def login(self, password):
if password == self.__password:
print(f"Welcome, {self.username}!")
else:
print("Wrong password")
user = UserAccount("riya_dev", "abc") # Password must be at least 8 characters!
user.set_password("python@123") # Password set successfully
user.login("hello") # Wrong password
user.login("python@123") # Welcome, riya_dev!Real-world note: Real applications never store plain-text passwords — they store a hash (e.g. using hashlib or Django's password hashers). This example only demonstrates encapsulation.
Don't run the code yet! Write your answers in a notebook, then check the Answers section at the end of this blog.
Q1. What is the output?
class A:
def __init__(self):
self.__x = 10
a = A()
print(a.__x)Q2. What is the output?
class A:
def __init__(self):
self.__x = 10
a = A()
print(a._A__x)Q3. What is the output?
class B:
def __init__(self):
self._y = 5
print(B()._y)Q4. How many underscores make an attribute private?
Q5. Is __init__ a private method?
Q6. What is the output?
class T:
def __init__(self):
self._c = 0
@property
def c(self):
return self._c
t = T()
t.c = 5Q7. Which decorator turns a method into a getter you can access like an attribute?
Solve these from easy to tricky:
Solutions for challenges 2 and 3 are in the Answers section.
Tick off each skill once you are confident:
Q: What is encapsulation in Python?
Bundling data and methods inside a class and restricting direct access to some of the data, allowing it to be changed only through controlled methods.
Q: Does Python support private variables?
Not strictly. A double underscore triggers name mangling (__x → _ClassName__x), which prevents accidental access but can still be bypassed.
Q: Difference between _x and __x?
_x is protected by convention only. __x is name-mangled, making it harder to access from outside and safe from accidental overriding in subclasses.
Q: Why use @property instead of get/set methods?
It keeps the simple attribute syntax (obj.x) while adding validation or computation, and you can add it later without changing the code that uses the class.
Q: Difference between encapsulation and abstraction?
Encapsulation hides data and controls access to it; abstraction hides implementation complexity and exposes only essential features.
| Concept | Syntax / Key point |
|---|---|
| Public | self.name |
| Protected | self._name |
| Private | self.__name |
| Mangled name | obj._Class__name |
| Getter | @property def x(self): |
| Setter | @x.setter def x(self, v): |
# Challenge 2
class Product:
def __init__(self, name, price):
self.name = name
self.price = price # goes through the setter
@property
def price(self):
return self.__price
@price.setter
def price(self, value):
if value < 0:
raise ValueError("Price cannot be negative")
self.__price = value
p = Product("Pen", 10)
p.price = 15
print(p.price) # 15
# Challenge 3
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!")
self._celsius = value
@property
def fahrenheit(self):
return self._celsius * 9 / 5 + 32
t = Temperature(25)
print(t.fahrenheit) # 77.0You can now protect your objects' data like a professional developer. Next, learn Polymorphism in Python — how the same method name can behave differently for different objects.
If this blog helped you, watch the full video above, subscribe to the channel and share it with a friend who is learning Python. Happy coding!