Python Encapsulation
Encapsulation is the bundling of data and methods that operate on that data within a single unit. Learn access modifiers, properties, data hiding, and encapsulation principles.
What is Encapsulation?
Encapsulation is one of the fundamental concepts in object-oriented programming. It refers to the bundling of data (attributes) and methods that operate on that data within a single unit (class), while restricting direct access to some of the object's components.
"Encapsulation hides the internal state and functionality of an object from the outside world."
Access Modifiers in Python
Python uses naming conventions for access control:
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner # Public attribute
self._balance = balance # Protected attribute (convention)
self.__pin = "1234" # Private attribute (name mangling)
def get_balance(self): # Public method
return self._balance
def _validate_pin(self, pin): # Protected method (convention)
return pin == self.__pin
def __change_pin(self, old_pin, new_pin): # Private method
if self._validate_pin(old_pin):
self.__pin = new_pin
return True
return False
account = BankAccount("John", 1000)
print(account.owner) # John (public)
print(account._balance) # 1000 (protected, but accessible)
# print(account.__pin) # AttributeError: 'BankAccount' object has no attribute '__pin'
print(account._BankAccount__pin) # 1234 (name mangling)
Properties for Data Encapsulation
Use properties to control attribute access:
class Temperature:
def __init__(self, celsius=0):
self._celsius = celsius
@property
def celsius(self):
"""Getter for celsius"""
return self._celsius
@celsius.setter
def celsius(self, value):
"""Setter for celsius with validation"""
if value < -273.15:
raise ValueError("Temperature cannot be below absolute zero")
self._celsius = value
@property
def fahrenheit(self):
"""Read-only property for fahrenheit"""
return self._celsius * 9/5 + 32
@fahrenheit.setter
def fahrenheit(self, value):
"""Setter for fahrenheit"""
self._celsius = (value - 32) * 5/9
temp = Temperature(25)
print(f"Celsius: {temp.celsius}") # 25
print(f"Fahrenheit: {temp.fahrenheit}") # 77.0
temp.celsius = 30
print(f"New Celsius: {temp.celsius}") # 30
print(f"New Fahrenheit: {temp.fahrenheit}") # 86.0
temp.fahrenheit = 100
print(f"Celsius from Fahrenheit: {temp.celsius}") # 37.777...
# This will raise ValueError
# temp.celsius = -300
Data Hiding and Validation
Encapsulate data with validation:
class Student:
def __init__(self, name, age, grade):
self._name = name
self._age = age
self._grade = grade
@property
def name(self):
return self._name
@name.setter
def name(self, value):
if not isinstance(value, str) or len(value.strip()) == 0:
raise ValueError("Name must be a non-empty string")
self._name = value.strip()
@property
def age(self):
return self._age
@age.setter
def age(self, value):
if not isinstance(value, int) or value < 5 or value > 100:
raise ValueError("Age must be an integer between 5 and 100")
self._age = value
@property
def grade(self):
return self._grade
@grade.setter
def grade(self, value):
valid_grades = ['A', 'B', 'C', 'D', 'F']
if value.upper() not in valid_grades:
raise ValueError(f"Grade must be one of {valid_grades}")
self._grade = value.upper()
student = Student("Alice", 16, "A")
print(f"Name: {student.name}, Age: {student.age}, Grade: {student.grade}")
student.name = "Bob"
student.age = 17
student.grade = "b" # Will be converted to "B"
print(f"Updated: {student.name}, {student.age}, {student.grade}")
Private Methods and Internal Logic
Hide implementation details:
class EmailValidator:
def __init__(self, email):
self.email = email
def is_valid(self):
"""Public method that uses private helper methods"""
if not self.__has_valid_format():
return False
if not self.__has_valid_domain():
return False
return True
def __has_valid_format(self):
"""Private method for format validation"""
return '@' in self.email and '.' in self.email.split('@')[1]
def __has_valid_domain(self):
"""Private method for domain validation"""
domain = self.email.split('@')[1]
valid_domains = ['gmail.com', 'yahoo.com', 'outlook.com']
return domain in valid_domains
validator = EmailValidator("user@gmail.com")
print(validator.is_valid()) # True
# Private methods are not accessible directly
# validator.__has_valid_format() # AttributeError
Class Variables and Encapsulation
Encapsulate class-level data:
class Employee:
_company_name = "Tech Corp" # Protected class variable
__employee_count = 0 # Private class variable
def __init__(self, name, salary):
self.name = name
self._salary = salary
Employee.__employee_count += 1
@classmethod
def get_employee_count(cls):
"""Public class method to access private class variable"""
return cls.__employee_count
@classmethod
def get_company_name(cls):
"""Public class method to access protected class variable"""
return cls._company_name
@classmethod
def set_company_name(cls, name):
"""Public class method to modify protected class variable"""
cls._company_name = name
emp1 = Employee("John", 50000)
emp2 = Employee("Jane", 60000)
print(Employee.get_employee_count()) # 2
print(Employee.get_company_name()) # Tech Corp
Employee.set_company_name("New Tech Corp")
print(Employee.get_company_name()) # New Tech Corp
Encapsulation with Composition
Use composition to hide complex objects:
class Engine:
def __init__(self, horsepower):
self.__horsepower = horsepower
self.__running = False
def start(self):
self.__running = True
return "Engine started"
def stop(self):
self.__running = False
return "Engine stopped"
def is_running(self):
return self.__running
def get_horsepower(self):
return self.__horsepower
class Car:
def __init__(self, make, model, horsepower):
self.make = make
self.model = model
self._engine = Engine(horsepower) # Encapsulated engine
def start_car(self):
return f"{self.make} {self.model}: {self._engine.start()}"
def stop_car(self):
return f"{self.make} {self.model}: {self._engine.stop()}"
def get_engine_power(self):
return self._engine.get_horsepower()
car = Car("Toyota", "Camry", 200)
print(car.start_car()) # Toyota Camry: Engine started
print(car.get_engine_power()) # 200
# Engine details are encapsulated
# car._engine.__horsepower # Not directly accessible
Read-Only Properties
Create immutable attributes:
class Person:
def __init__(self, name, birth_year):
self._name = name
self._birth_year = birth_year
@property
def name(self):
return self._name
@property
def birth_year(self):
return self._birth_year
@property
def age(self):
"""Read-only property calculated from birth year"""
from datetime import datetime
current_year = datetime.now().year
return current_year - self._birth_year
person = Person("Alice", 1990)
print(f"Name: {person.name}") # Alice
print(f"Birth Year: {person.birth_year}") # 1990
print(f"Age: {person.age}") # Current age
# These will raise AttributeError (no setters defined)
# person.name = "Bob"
# person.birth_year = 1985
# person.age = 30
Encapsulation Patterns
# Data Transfer Object (DTO) Pattern
class UserDTO:
def __init__(self, user_id, username, email):
self._user_id = user_id
self._username = username
self._email = email
@property
def user_id(self):
return self._user_id
@property
def username(self):
return self._username
@username.setter
def username(self, value):
if len(value) < 3:
raise ValueError("Username must be at least 3 characters")
self._username = value
@property
def email(self):
return self._email
@email.setter
def email(self, value):
if '@' not in value:
raise ValueError("Invalid email format")
self._email = value
# Builder Pattern with Encapsulation
class Computer:
def __init__(self):
self._cpu = None
self._ram = None
self._storage = None
def set_cpu(self, cpu):
self._cpu = cpu
return self
def set_ram(self, ram):
self._ram = ram
return self
def set_storage(self, storage):
self._storage = storage
return self
def build(self):
if not all([self._cpu, self._ram, self._storage]):
raise ValueError("All components must be set")
return ComputerBuilt(self._cpu, self._ram, self._storage)
class ComputerBuilt:
def __init__(self, cpu, ram, storage):
self.__cpu = cpu
self.__ram = ram
self.__storage = storage
def get_specs(self):
return {
'cpu': self.__cpu,
'ram': self.__ram,
'storage': self.__storage
}
computer = Computer().set_cpu("Intel i7").set_ram("16GB").set_storage("512GB").build()
print(computer.get_specs())
Best Practices
- Use private attributes for internal state: Prefix with __
- Provide public properties for access: Use @property decorator
- Validate data in setters: Ensure data integrity
- Keep implementation details private: Hide complexity
- Use protected attributes sparingly: Prefix with _ for inheritance
- Document encapsulation decisions: Explain why data is hidden
- Avoid over-encapsulation: Don't hide everything
- Use composition for complex objects: Encapsulate subsystems
- Provide meaningful interfaces: Clear public methods
- Test encapsulation thoroughly: Verify data protection
Common Mistakes to Avoid
- Accessing private attributes directly: Use name mangling only when necessary
- Making everything private: Balance encapsulation with usability
- Ignoring validation: Always validate input data
- Exposing internal objects: Return copies, not references
- Mixing access levels: Be consistent in your approach
Encapsulation is crucial for creating robust, maintainable code. It protects the internal state of objects and provides controlled access to data, making your code more secure and easier to modify.