Python Encapsulation

In the last lesson, we learned about Python Polymorphism. After learning about polymorphism, let us learn about encapsulation in Python. Encapsulation is about keeping data and methods together, and hiding internal details.

In this tutorial, we will learn what encapsulation is, how to use it, and why it is useful.

๐Ÿงฉ What Is Encapsulation in Python?

Encapsulation is the practice of keeping data and methods together inside a class. And hiding the internal details from the outside.

Think of it like a car. We drive the car using the steering wheel, pedals, and gear stick. We do not need to know how the engine works. The engine is hidden. That is encapsulation.

In Python, encapsulation means:

  • Keeping data (attributes) and methods together in a class
  • Restricting direct access to some data
  • Providing methods to access and modify data safely

๐Ÿ”ง Why Do We Need Encapsulation in Python?

Encapsulation protects data from accidental changes. If we allow direct access to all data, someone might set an invalid value. That could break our program.

Encapsulation also makes our code easier to maintain. We can change the internal details without affecting the outside code.

๐Ÿ”’ Public, Protected, and Private Members in Python

Python does not have strict access control like some other languages. But it has conventions.

Public โ€” Accessible from anywhere. No underscore.

Protected โ€” Should only be accessed within the class and subclasses. One underscore _.

Private โ€” Should only be accessed within the class. Two underscores __.

Let me show you.

class Person:
def __init__(self, name, age, salary):
self.name = name # public
self._age = age # protected
self.__salary = salary # private
person = Person("Alex", 25, 50000)
print(person.name) # Works
print(person._age) # Works, but should be avoided
print(person.__salary) # Error!

Output

Alex
25
AttributeError: 'Person' object has no attribute '__salary'

The private attribute __salary cannot be accessed directly from outside. Python does name mangling. It changes the name internally to _Person__salary.

We can still access it if we know the mangled name. But we should not.

print(person._Person__salary) # Works, but bad practice

๐Ÿ’ก How to Use Encapsulation in Python

We use encapsulation by making attributes private and providing methods to access them.

class Person:
def __init__(self, name, age):
self.name = name
self.__age = age
def get_age(self):
return self.__age
def set_age(self, age):
if age > 0:
self.__age = age
else:
print("Age cannot be negative")
person = Person("Alex", 25)
print(person.get_age())
person.set_age(30)
print(person.get_age())
person.set_age(-5)
print(person.get_age())

Output

25
30
Age cannot be negative
30

The set_age method checks if the age is valid before setting it. That protects the data.

๐Ÿงช Getters and Setters in Python

Getters and setters are methods used to access and modify private attributes.

  • Getter โ€” returns the value
  • Setter โ€” sets the value, often with validation
class Student:
def __init__(self, name, grade):
self.name = name
self.__grade = grade
def get_grade(self):
return self.__grade
def set_grade(self, grade):
if 0 <= grade <= 100:
self.__grade = grade
else:
print("Grade must be between 0 and 100")
student = Student("Alex", 85)
print(student.get_grade())
student.set_grade(95)
print(student.get_grade())
student.set_grade(150)
print(student.get_grade())

Output

85
95
Grade must be between 0 and 100
95

That is encapsulation. The grade is protected. And we control how it is set.

๐Ÿ“Œ Using @property for Getters and Setters in Python

Python has a nicer way to do getters and setters. We use the @property decorator.

class Person:
def __init__(self, name, age):
self.name = name
self._age = age
@property
def age(self):
return self._age
@age.setter
def age(self, age):
if age > 0:
self._age = age
else:
print("Age cannot be negative")
person = Person("Alex", 25)
print(person.age)
person.age = 30
print(person.age)
person.age = -5
print(person.age)

Output

25
30
Age cannot be negative
30

Now we can use person.age like a normal attribute. But it still goes through the getter and setter. That is clean and Pythonic.

๐ŸŒ A Real-Life Example โ€“ Bank Account

class BankAccount:
def __init__(self, owner, balance):
self.owner = owner
self.__balance = balance
@property
def balance(self):
return self.__balance
def deposit(self, amount):
if amount > 0:
self.__balance += amount
print(f"Deposited {amount}. New balance: {self.__balance}")
else:
print("Deposit amount must be positive")
def withdraw(self, amount):
if amount > self.__balance:
print("Insufficient funds")
elif amount <= 0:
print("Withdrawal amount must be positive")
else:
self.__balance -= amount
print(f"Withdrew {amount}. New balance: {self.__balance}")
account = BankAccount("Alex", 1000)
print(account.balance)
account.deposit(500)
account.withdraw(200)
account.withdraw(2000)

Output

1000
Deposited 500. New balance: 1500
Withdrew 200. New balance: 1300
Insufficient funds

The balance is private. It can only be changed through deposit and withdraw methods. That is encapsulation.

โš™๏ธ Common Mistakes with Encapsulation in Python

โŒ Mistake 1: Thinking private is truly private

person._Person__salary = 100000

This works. Python does not enforce privacy. It is just a convention.

Fix: Respect the convention. Do not access private attributes from outside.

๐Ÿšซ Mistake 2: Using one underscore for private

self._age = age # protected, not private

One underscore means protected. It can still be accessed from outside.

Fix: Use two underscores for private.

self.__age = age

๐Ÿ›‘ Mistake 3: Forgetting to use the setter

class Person:
def __init__(self, age):
self._age = age
@property
def age(self):
return self._age
@age.setter
def age(self, age):
self._age = age
person = Person(25)
person._age = -5 # bypasses the setter

This bypasses the validation. Fix: Always use the property.

person.age = -5

Next, we will learn about Python Abstraction and hide complex details behind a simple interface.