Table of Contents
Understanding Encapsulation in Python
Encapsulation is about bundling data and the code that works with that data together, and controlling access to that data.
In Python classes, encapsulation usually means:
- Keeping related data and behavior together inside a class
- Hiding internal details that other code should not touch directly
- Providing a clear interface (methods) for interacting with an object
Why Encapsulation Is Useful
Encapsulation helps you:
- Protect data from accidental changes
You decide which parts of an object can be changed directly, and which should only be changed in controlled ways.
- Make code easier to understand
Other parts of the program do not need to know how something is done, only how to use it.
- Make changes without breaking everything
You can change how a class works on the inside, as long as its public interface (methods and attributes you promised) stays the same.
A common idea in encapsulation is: “Don’t expose more than you must.”
Public, “Protected”, and “Private” (Python Style)
Python does not enforce strict access control like some other languages. Instead, it uses naming conventions to signal how attributes and methods should be used.
Public Attributes and Methods
- No leading underscore
- Intended to be used from outside the class
Example:
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner # public
self.balance = balance # public
def deposit(self, amount): # public
self.balance += amount
Code outside the class is “allowed” to use owner, balance, and deposit:
account = BankAccount("Alice", 100)
print(account.balance) # allowed (public)
account.deposit(50) # allowed (public)“Protected” (Single Underscore)
- Starts with one underscore:
_name - Convention: “This is internal, please don’t touch it directly.”
- Not enforced by Python, just a warning to other programmers.
Example:
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner
self._balance = balance # "protected" by convention
def deposit(self, amount):
self._balance += amount
def get_balance(self):
return self._balance
You can still access _balance from outside, but you shouldn’t:
account = BankAccount("Alice", 100)
print(account.get_balance()) # recommended
print(account._balance) # possible, but not recommended“Private” (Double Underscore)
- Starts with two leading underscores:
__name - Python does name mangling: internally renames it to include the class name.
- Makes it harder to access from outside the class (but still not truly impossible).
Example:
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner
self.__balance = balance # "private"
def deposit(self, amount):
if amount > 0:
self.__balance += amount
def get_balance(self):
return self.__balance
Trying to access __balance directly:
account = BankAccount("Alice", 100)
print(account.get_balance()) # OK
print(account.__balance) # AttributeError
Internally, Python stored it as _BankAccount__balance; you could access it like this, but you should not in normal code:
print(account._BankAccount__balance) # works, but bad practiceThe idea is: double underscores strongly signal “don’t touch this from outside”.
Hiding Internal Details with Methods
Encapsulation is not just about underscores; it’s about controlling how data is changed.
Instead of letting code directly change attributes, you can:
- Keep attributes “hidden” (with
_or__) - Provide methods to safely read or change them
Example: only allow valid ages for a person:
class Person:
def __init__(self, name, age):
self.name = name
self.__age = 0
self.set_age(age)
def set_age(self, age):
if 0 <= age <= 120:
self.__age = age
else:
print("Invalid age, keeping old value.")
def get_age(self):
return self.__ageUsage:
p = Person("Bob", 30)
print(p.get_age()) # 30
p.set_age(200) # Invalid age, keeping old value.
print(p.get_age()) # still 30
# Direct access blocked:
# p.__age -> AttributeError
The Person class encapsulates the age data and decides what values are allowed.
Encapsulation and “Information Hiding”
A typical pattern:
- Expose a small, clear set of public methods (the interface)
- Hide the details inside the class (private/protected attributes, helper methods)
Example: a simple counter that other code can use but not mess up:
class Counter:
def __init__(self):
self.__value = 0 # internal state
def increment(self):
self.__value += 1
def reset(self):
self.__value = 0
def get_value(self):
return self.__valueFrom outside:
c = Counter()
c.increment()
c.increment()
print(c.get_value()) # 2
# There is no direct way (on purpose) to set the value to something random:
# c.__value = 999 # doesn't touch the internal value; creates a new attributeHere:
- You only know you can
increment(),reset(), andget_value() - You don’t care how
Counterstores its value internally
Using Properties for Encapsulation
Python offers properties to make encapsulation more convenient. With a property, you can use attribute syntax (obj.x) but still have getter/setter logic behind the scenes.
Basic pattern:
- Keep the real attribute “hidden” (e.g.
__age) - Expose a property with the same name but with logic
Example:
class Person:
def __init__(self, name, age):
self.name = name
self.__age = 0
self.age = age # will use the property setter
@property
def age(self):
# getter
return self.__age
@age.setter
def age(self, value):
# setter with validation
if 0 <= value <= 120:
self.__age = value
else:
print("Invalid age, keeping old value.")Usage:
p = Person("Alice", 25)
print(p.age) # uses getter -> 25
p.age = 30 # uses setter
print(p.age) # 30
p.age = 200 # Invalid age, keeping old value.
print(p.age) # still 30
From the outside, age looks like a simple attribute, but it is encapsulated with validation logic.
Practical Encapsulation Example: Bank Account
Putting it together:
- Hide the balance
- Validate operations
- Provide a clear interface
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner # public
self.__balance = 0 # "private"
self.deposit(balance) # reuse method for validation
@property
def balance(self):
# read-only property (no setter)
return self.__balance
def deposit(self, amount):
if amount <= 0:
print("Deposit must be positive.")
return
self.__balance += amount
def withdraw(self, amount):
if amount <= 0:
print("Withdrawal must be positive.")
return
if amount > self.__balance:
print("Insufficient funds.")
return
self.__balance -= amountUsage:
account = BankAccount("Alice", 100)
print(account.balance) # 100
account.deposit(50)
print(account.balance) # 150
account.withdraw(200) # Insufficient funds.
print(account.balance) # 150
# account.balance = 1000 # AttributeError: no setter defined
# account.__balance # AttributeError: private attributeIn this design:
- The internal state (
__balance) is protected from direct changes - All changes must go through
depositorwithdraw - The
balanceproperty provides read-only access
Encapsulation and Good Class Design
When you design classes, ask:
- What should other code be allowed to do with this object?
- These become public methods and attributes.
- What should other code not touch directly?
- These become “private” or “protected” attributes/methods (using
_or__). - Do I need validation or rules for changes?
- Use methods or properties instead of exposing raw attributes.
Encapsulation helps you keep your classes clean, safe, and easier to use, and it becomes more and more important as your programs grow larger.