- What is encapsulation ā bundling data and methods that work on that data
- Data hiding ā protecting internal state from direct access
- Access modifiers ā public, protected, and private in Python
- Getters and setters ā controlling access to attributes
- The @property decorator ā Python's elegant way to create getters and setters
- Real-world use ā practical examples you can use
What is Encapsulation?
Encapsulation is one of the four pillars of Object-Oriented Programming (along with inheritance, polymorphism, and abstraction). It's about bundling data and the methods that work on that data together, and controlling access to that data from outside the class.
Think of encapsulation like a vending machine. You interact with the machine through a clear interface ā you put in money, press buttons, and get your snack. But you don't interact directly with the internal mechanisms ā the coin sensors, the motor, the conveyor belt. Those are encapsulated inside the machine.
In Python, encapsulation is implemented through:
- Access modifiers ā public, protected, private conventions
- Getters and setters ā methods that control access to attributes
- The @property decorator ā Python's elegant way to create controlled access
š” Key concept: Encapsulation is about what you expose to the outside world, not how it works internally. It's about protecting your data and providing a clean interface.
Why Encapsulate?
The Benefits of Encapsulation
Encapsulation isn't just a fancy OOP concept ā it has real, practical benefits that make your code better.
# Why Encapsulation Matters
print("=" * 50)
print("WHY ENCAPSULATE?")
print("=" * 50)
# ============================================================
# WITHOUT ENCAPSULATION ā The Problem
# ============================================================
print("\nā WITHOUT ENCAPSULATION:")
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner
self.balance = balance # Public ā anyone can change it!
# Everything is public ā no protection
account = BankAccount("Alice", 1000)
print(f"Initial balance: ${account.balance}")
# Anyone can directly modify the balance
account.balance = -500 # Negative balance! š±
print(f"After direct modification: ${account.balance}")
# No validation, no protection, no consistency
account.balance = "abc" # String instead of number! š±
print(f"After assigning a string: ${account.balance}")
print("This will cause errors when trying to do math with it!")
print("\nā Problems:")
print(" 1. No validation ā can set invalid values")
print(" 2. No protection ā can break the object's state")
print(" 3. No consistency ā no way to ensure rules are followed")
# ============================================================
# WITH ENCAPSULATION ā The Solution
# ============================================================
print("\nā
WITH ENCAPSULATION:")
class SecureBankAccount:
def __init__(self, owner, initial_balance):
self._owner = owner
self._balance = 0 # Protected
self._transaction_history = []
# Use the setter method to validate
self.deposit(initial_balance)
def deposit(self, amount):
"""Add money to the account with validation"""
if amount <= 0:
raise ValueError("Deposit amount must be positive")
self._balance += amount
self._transaction_history.append(f"Deposited: +${amount}")
return f"Deposited ${amount}. New balance: ${self._balance}"
def withdraw(self, amount):
"""Withdraw money with validation"""
if amount <= 0:
raise ValueError("Withdrawal amount must be positive")
if amount > self._balance:
raise ValueError(f"Insufficient funds. Balance: ${self._balance}")
self._balance -= amount
self._transaction_history.append(f"Withdrew: -${amount}")
return f"Withdrew ${amount}. New balance: ${self._balance}"
def get_balance(self):
"""Get balance ā read-only access"""
return self._balance
def get_owner(self):
"""Get owner ā read-only access"""
return self._owner
def get_transactions(self):
"""Get transaction history ā returns a copy to prevent modification"""
return self._transaction_history.copy()
# Using the encapsulated class
secure_account = SecureBankAccount("Bob", 1000)
print(f"Owner: {secure_account.get_owner()}")
print(f"Balance: ${secure_account.get_balance()}")
print(f"\n{secure_account.deposit(500)}")
print(f"{secure_account.withdraw(200)}")
# Can't directly access or modify the balance
# secure_account._balance = -500 # Would work but is discouraged
# print(secure_account._balance) # Accessible but not recommended
print("\nā
Benefits of Encapsulation:")
print(" 1. Validation ā prevents invalid data")
print(" 2. Protection ā controls how data can be changed")
print(" 3. Consistency ā ensures data always follows rules")
print(" 4. Maintainability ā can change internal logic without breaking external code")
print(" 5. Clear interface ā users know exactly what methods to call")
Benefits of encapsulation:
- Data protection ā prevents invalid or inconsistent data
- Validation ā ensures data meets requirements before being stored
- Maintainability ā internal changes don't affect external code
- Clear interface ā users know what methods to call
- Reduces bugs ā prevents accidental misuse of data
Quick Check: What's the main benefit of encapsulation? (Answer: It protects data and ensures it remains valid and consistent)
Access Modifiers in Python
Public, Protected, and Private
Python uses naming conventions to indicate the intended visibility of class members:
- Public ā
name(no underscore) ā accessible from anywhere - Protected ā
_name(single underscore) ā intended for internal use - Private ā
__name(double underscore) ā name mangled, harder to access
Remember: Python's access modifiers are conventions, not strict enforcement. Python trusts developers to respect the conventions.
# Access Modifiers in Python
print("=" * 50)
print("ACCESS MODIFIERS IN PYTHON")
print("=" * 50)
class AccessDemo:
"""Demonstrates public, protected, and private members"""
def __init__(self):
# Public ā no underscore, accessible everywhere
self.public = "I'm public"
# Protected ā single underscore, for internal use
self._protected = "I'm protected"
# Private ā double underscore, name mangled
self.__private = "I'm private"
def public_method(self):
"""Public method ā accessible everywhere"""
return "Public method called"
def _protected_method(self):
"""Protected method ā for internal use"""
return "Protected method called"
def __private_method(self):
"""Private method ā name mangled"""
return "Private method called"
def access_all_inside(self):
"""Inside the class ā all are accessible"""
return {
"public": self.public,
"protected": self._protected,
"private": self.__private,
"public_method": self.public_method(),
"protected_method": self._protected_method(),
"private_method": self.__private_method()
}
# ============================================================
# DEMONSTRATION
# ============================================================
demo = AccessDemo()
print("\n1. INSIDE THE CLASS ā ALL ACCESSIBLE")
inside = demo.access_all_inside()
for key, value in inside.items():
print(f" {key}: {value}")
print("\n2. OUTSIDE THE CLASS ā VISIBILITY")
print(f" Public: {demo.public}")
print(f" Public method: {demo.public_method()}")
# Protected ā accessible but discouraged
print(f" Protected: {demo._protected}")
print(f" Protected method: {demo._protected_method()}")
# Private ā NOT directly accessible
try:
print(demo.__private)
except AttributeError as e:
print(f" ā Private: {e}")
try:
print(demo.__private_method())
except AttributeError as e:
print(f" ā Private method: {e}")
print("\n3. NAME MANGLING ā PRIVATE ACCESS (HACK)")
print(f" Mangled private: {demo._AccessDemo__private}")
print(f" Mangled private method: {demo._AccessDemo__private_method()}")
print("\n" + "-" * 30)
print("ACCESS MODIFIERS SUMMARY")
print("-" * 30)
print("""
āāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Modifier ā Syntax ā Visibility ā
āāāāāāāāāāāāāāāāāāā¼āāāāāāāāāāāāāāāāāāā¼āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā Public ā name ā Accessible everywhere ā
āāāāāāāāāāāāāāāāāāā¼āāāāāāāāāāāāāāāāāāā¼āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā Protected ā _name ā For internal use (convention) ā
āāāāāāāāāāāāāāāāāāā¼āāāāāāāāāāāāāāāāāāā¼āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā Private ā __name ā Name mangled (harder to access) ā
āāāāāāāāāāāāāāāāāāā“āāāāāāāāāāāāāāāāāāā“āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
š Remember: These are CONVENTIONS, not strict enforcement.
Python trusts developers to "be consenting adults."
""")
Access modifiers key points:
- Public ā no underscore, intended for external use
- Protected ā single underscore, for internal use (subclasses can access)
- Private ā double underscore, name mangling prevents accidental access
- Conventions, not enforcement ā Python trusts developers
- Name mangling ā
__namebecomes_ClassName__name
Quick Check: How do you indicate a private member in Python? (Answer: Use a double underscore prefix ā __name)
Getters and Setters
Controlled Access to Attributes
Getters and setters are methods that control access to an object's attributes. They let you add logic ā like validation, logging, or transformation ā when someone gets or sets a value.
In many languages, getters and setters are written explicitly. In Python, we often use the @property decorator instead, but it's helpful to understand the traditional approach first.
# Traditional Getters and Setters
print("=" * 50)
print("GETTERS AND SETTERS")
print("=" * 50)
class Person:
"""Person with traditional getters and setters"""
def __init__(self, name, age):
self._name = name
self._age = age
# ----- GETTERS -----
def get_name(self):
"""Get the person's name"""
return self._name
def get_age(self):
"""Get the person's age"""
return self._age
# ----- SETTERS -----
def set_name(self, name):
"""Set the person's name with validation"""
if not name or not name.strip():
raise ValueError("Name cannot be empty")
self._name = name.strip()
return f"Name updated to: {self._name}"
def set_age(self, age):
"""Set the person's age with validation"""
if not isinstance(age, int):
raise TypeError("Age must be an integer")
if age < 0:
raise ValueError("Age cannot be negative")
if age > 150:
raise ValueError("Age must be realistic (under 150)")
self._age = age
return f"Age updated to: {self._age}"
# ----- OTHER METHODS -----
def get_info(self):
"""Get complete info"""
return f"Name: {self._name}, Age: {self._age}"
# ============================================================
# DEMONSTRATION
# ============================================================
person = Person("Alice", 30)
print("1. GETTING VALUES")
print(f" Name: {person.get_name()}")
print(f" Age: {person.get_age()}")
print(f" Info: {person.get_info()}")
print("\n2. SETTING VALUES (VALID)")
print(f" {person.set_name('Bob')}")
print(f" {person.set_age(35)}")
print(f" Info: {person.get_info()}")
print("\n3. SETTING VALUES (INVALID)")
try:
person.set_age(-5)
except ValueError as e:
print(f" ā {e}")
try:
person.set_name("")
except ValueError as e:
print(f" ā {e}")
try:
person.set_age("thirty")
except TypeError as e:
print(f" ā {e}")
print("\n" + "-" * 30)
print("GETTERS AND SETTERS ā PROS AND CONS")
print("-" * 30)
print("""
ā
PROS:
⢠Full control over attribute access
⢠Can add validation and business logic
⢠Can change internal representation without breaking external code
⢠Can add logging, caching, or other cross-cutting concerns
ā CONS:
⢠Verbose ā lots of boilerplate code
⢠Less Pythonic ā other Python developers expect properties
⢠Can make code harder to read
⢠The @property decorator is usually a better choice
""")
Getters and setters key points:
- Getters ā methods that retrieve attribute values
- Setters ā methods that set attribute values with validation
- Control ā add logic like validation, logging, transformation
- Encapsulation ā hide internal representation
- Verbose ā can be a lot of code for simple cases
Quick Check: What's the main advantage of using getters and setters? (Answer: They allow you to add validation and logic when accessing or modifying attributes)
The @property Decorator
Python's Elegant Solution
The @property decorator is Python's elegant way to create getters and setters. It lets you define methods that can be accessed like attributes ā without the need for explicit getter/setter method calls.
This is the Pythonic way to implement encapsulation. It gives you the best of both worlds: simple attribute-like syntax with full control over access.
# The @property Decorator ā Pythonic Encapsulation
print("=" * 50)
print("THE @property DECORATOR")
print("=" * 50)
class Temperature:
"""Temperature with property-based encapsulation"""
def __init__(self, celsius):
self._celsius = celsius
self._fahrenheit = celsius * 9/5 + 32
# ----- PROPERTY GETTER -----
@property
def celsius(self):
"""Get temperature in Celsius"""
return self._celsius
@celsius.setter
def celsius(self, value):
"""Set temperature in Celsius with validation"""
if not isinstance(value, (int, float)):
raise TypeError("Temperature must be a number")
if value < -273.15:
raise ValueError("Temperature cannot be below absolute zero (-273.15°C)")
self._celsius = value
self._fahrenheit = value * 9/5 + 32
@property
def fahrenheit(self):
"""Get temperature in Fahrenheit"""
return self._fahrenheit
@fahrenheit.setter
def fahrenheit(self, value):
"""Set temperature in Fahrenheit with validation"""
if not isinstance(value, (int, float)):
raise TypeError("Temperature must be a number")
celsius = (value - 32) * 5/9
if celsius < -273.15:
raise ValueError("Temperature cannot be below absolute zero")
self._celsius = celsius
self._fahrenheit = value
@property
def kelvin(self):
"""Get temperature in Kelvin (read-only property)"""
return self._celsius + 273.15
def __repr__(self):
return f"Temperature({self._celsius}°C / {self._fahrenheit}°F)"
# ============================================================
# DEMONSTRATION
# ============================================================
temp = Temperature(25)
print("1. GETTING VALUES (like attributes)")
print(f" Celsius: {temp.celsius}°C")
print(f" Fahrenheit: {temp.fahrenheit}°F")
print(f" Kelvin: {temp.kelvin}K")
print(f" {temp}")
print("\n2. SETTING VALUES (with validation)")
temp.celsius = 30
print(f" After setting celsius=30: {temp}")
print(f" Fahrenheit automatically updated: {temp.fahrenheit}°F")
temp.fahrenheit = 100
print(f" After setting fahrenheit=100: {temp}")
print(f" Celsius automatically updated: {temp.celsius}°C")
print("\n3. TRYING INVALID VALUES")
try:
temp.celsius = -300
except ValueError as e:
print(f" ā {e}")
try:
temp.celsius = "hot"
except TypeError as e:
print(f" ā {e}")
print("\n4. READ-ONLY PROPERTY")
print(f" Kelvin (read-only): {temp.kelvin}K")
try:
temp.kelvin = 300
except AttributeError as e:
print(f" ā Cannot set kelvin: {e}")
print("\n" + "-" * 30)
print("@property BENEFITS:")
print("-" * 30)
print("""
ā
Clean syntax: access like attributes (obj.attribute)
ā
Encapsulation: control access with getters/setters
ā
Validation: add rules when setting values
ā
Read-only: properties without setters are read-only
ā
Computed values: calculate on the fly
ā
Pythonic: this is the Python way to do encapsulation
š The @property decorator is the recommended way to
implement encapsulation in Python.
""")
@property key points:
- Clean syntax ā access like attributes, not methods
- Validation ā add logic in the setter
- Read-only ā define property without a setter
- Computed values ā calculate values on the fly
- Pythonic ā the preferred way to implement encapsulation
Quick Check: What decorator is used to create Pythonic getters and setters? (Answer: @property)
Real-World Example
Building a User Management System
# Real-World Example: User Management System
import re
import hashlib
import secrets
from datetime import datetime
print("=" * 60)
print("USER MANAGEMENT SYSTEM")
print("=" * 60)
class User:
"""Secure user management with encapsulation"""
def __init__(self, username, email, password):
self._username = None
self._email = None
self._password_hash = None
self._created_at = datetime.now()
self._last_login = None
self._is_active = True
self._failed_attempts = 0
self._session_token = None
# Use setters for validation
self.username = username
self.email = email
self.password = password
# ----- PROPERTIES -----
@property
def username(self):
"""Get username (read-only once set)"""
return self._username
@username.setter
def username(self, value):
"""Set username with validation"""
if not value or not value.strip():
raise ValueError("Username cannot be empty")
if len(value) < 3:
raise ValueError("Username must be at least 3 characters")
if not re.match(r'^[a-zA-Z0-9_]+$', value):
raise ValueError("Username can only contain letters, numbers, and underscores")
if self._username is not None:
raise ValueError("Username cannot be changed after creation")
self._username = value.strip()
@property
def email(self):
"""Get email"""
return self._email
@email.setter
def email(self, value):
"""Set email with validation"""
if not value or not value.strip():
raise ValueError("Email cannot be empty")
# Simple email validation
if '@' not in value or '.' not in value:
raise ValueError("Invalid email format")
if self._email is not None:
raise ValueError("Email cannot be changed after creation")
self._email = value.strip()
@property
def password(self):
"""Password getter (not allowed)"""
raise AttributeError("Password is not accessible for security reasons")
@password.setter
def password(self, value):
"""Set password with hashing"""
if not value or len(value) < 8:
raise ValueError("Password must be at least 8 characters")
# Hash the password
salt = secrets.token_hex(16)
self._password_hash = hashlib.sha256((value + salt).encode()).hexdigest()
@property
def created_at(self):
"""Get creation time (read-only)"""
return self._created_at
@property
def last_login(self):
"""Get last login time (read-only)"""
return self._last_login
@property
def is_active(self):
"""Check if account is active"""
return self._is_active
@property
def session_token(self):
"""Get session token"""
return self._session_token
# ----- PUBLIC METHODS -----
def login(self, password):
"""Authenticate user"""
if not self._is_active:
return "Account is locked. Contact support."
# Check password (simplified)
# In real code, you'd verify the hash properly
if len(password) >= 8:
self._failed_attempts = 0
self._last_login = datetime.now()
self._session_token = secrets.token_urlsafe(32)
return f"Welcome back, {self._username}!"
else:
self._failed_attempts += 1
if self._failed_attempts >= 3:
self._is_active = False
return "Account locked due to too many failed attempts."
return f"Invalid password. {3 - self._failed_attempts} attempts remaining."
def logout(self):
"""Log out user"""
self._session_token = None
return "Logged out successfully."
def change_password(self, old_password, new_password):
"""Change password with verification"""
# Verify old password (simplified)
if len(old_password) < 8:
return "Invalid current password."
if len(new_password) < 8:
return "New password must be at least 8 characters."
self.password = new_password
self._session_token = None
return "Password changed successfully."
def get_profile(self):
"""Get user profile (read-only public view)"""
return {
"username": self._username,
"email": self._email,
"created_at": self._created_at.strftime("%Y-%m-%d %H:%M"),
"last_login": self._last_login.strftime("%Y-%m-%d %H:%M") if self._last_login else "Never",
"is_active": self._is_active
}
# ============================================================
# DEMONSTRATION
# ============================================================
print("\n1. CREATING A USER (with validation)")
try:
user = User("alice123", "alice@example.com", "SecurePass123")
print(" ā
User created successfully!")
print(f" Username: {user.username}")
print(f" Email: {user.email}")
print(f" Created: {user.created_at.strftime('%Y-%m-%d %H:%M')}")
except ValueError as e:
print(f" ā Error: {e}")
print("\n2. TRYING INVALID USERNAME")
try:
user2 = User("ab", "bob@example.com", "Password123")
except ValueError as e:
print(f" ā {e}")
print("\n3. TRYING INVALID EMAIL")
try:
user3 = User("bob123", "invalid-email", "Password123")
except ValueError as e:
print(f" ā {e}")
print("\n4. TRYING WEAK PASSWORD")
try:
user4 = User("charlie", "charlie@example.com", "123")
except ValueError as e:
print(f" ā {e}")
print("\n5. LOGIN ATTEMPTS")
print(f" {user.login('wrong_password')}")
print(f" {user.login('wrong_password')}")
print(f" {user.login('SecurePass123')}") # Success!
print("\n6. GETTING PROFILE")
profile = user.get_profile()
print(f" Profile: {profile}")
print("\n7. ACCESSING PASSWORD (Blocked)")
try:
print(user.password)
except AttributeError as e:
print(f" ā {e}")
print("\n8. TRYING TO CHANGE USERNAME (Blocked)")
try:
user.username = "new_alice"
except ValueError as e:
print(f" ā {e}")
print("\n" + "=" * 60)
print("KEY TAKEAWAYS:")
print("=" * 60)
print("ā
Properties provide clean, attribute-like access")
print("ā
Validation ensures data integrity")
print("ā
Sensitive data (password) is protected")
print("ā
Read-only properties expose what's needed")
print("ā
Internal state is hidden from outside")
Real-world example key points:
- Validation ā username, email, and password all have validation
- Read-only properties ā created_at, last_login, session_token
- Protected data ā password is hashed and not accessible
- Clear interface ā users know what methods to call
- Security ā failed attempts tracking, account locking
Quick Check: How is the password protected in this example? (Answer: It's hashed, and there's no getter ā attempting to access it raises an AttributeError)
Best Practices
Using Encapsulation Effectively
# Best Practices for Encapsulation
print("=" * 60)
print("BEST PRACTICES FOR ENCAPSULATION")
print("=" * 60)
# ============================================================
# 1. USE @property FOR CONTROLLED ACCESS
# ============================================================
print("\n1. USE @property FOR CONTROLLED ACCESS")
class Product:
"""Product with property-based encapsulation"""
def __init__(self, name, price):
self._name = name
self._price = price
self._discount = 0
@property
def name(self):
return self._name
@property
def price(self):
"""Price with discount applied"""
return self._price * (1 - self._discount / 100)
@price.setter
def price(self, value):
if value < 0:
raise ValueError("Price cannot be negative")
self._price = value
@property
def discount(self):
return self._discount
@discount.setter
def discount(self, value):
if not 0 <= value <= 100:
raise ValueError("Discount must be between 0 and 100")
self._discount = value
print(" ā
Use @property for attribute-like access with control")
# ============================================================
# 2. MAKE ATTRIBUTES PRIVATE UNLESS NEEDED
# ============================================================
print("\n2. MAKE ATTRIBUTES PRIVATE UNLESS NEEDED")
class BankAccount:
def __init__(self, balance):
self._balance = balance # Protected
self.__account_number = self._generate_account_number() # Private
self._transactions = [] # Protected
def _generate_account_number(self):
import random
return f"ACC-{random.randint(10000, 99999)}"
@property
def balance(self):
return self._balance
def deposit(self, amount):
self._balance += amount
self._transactions.append(f"+{amount}")
def withdraw(self, amount):
if amount <= self._balance:
self._balance -= amount
self._transactions.append(f"-{amount}")
print(" ā
Use protected (_) for internal attributes")
print(" ā
Use private (__) for sensitive data")
# ============================================================
# 3. VALIDATE IN SETTERS
# ============================================================
print("\n3. VALIDATE IN SETTERS")
class Student:
def __init__(self, name, grade):
self._name = name
self._grade = grade
@property
def grade(self):
return self._grade
@grade.setter
def grade(self, value):
if not 0 <= value <= 100:
raise ValueError("Grade must be between 0 and 100")
self._grade = value
try:
s = Student("Alice", 85)
print(f" Grade: {s.grade}")
s.grade = 95 # Valid
print(f" Updated grade: {s.grade}")
s.grade = 150 # Invalid
except ValueError as e:
print(f" ā {e}")
# ============================================================
# 4. USE READ-ONLY PROPERTIES FOR COMPUTED VALUES
# ============================================================
print("\n4. USE READ-ONLY PROPERTIES FOR COMPUTED VALUES")
class Rectangle:
def __init__(self, width, height):
self._width = width
self._height = height
@property
def area(self):
"""Computed property (read-only)"""
return self._width * self._height
@property
def perimeter(self):
"""Computed property (read-only)"""
return 2 * (self._width + self._height)
rect = Rectangle(5, 3)
print(f" Area: {rect.area}")
print(f" Perimeter: {rect.perimeter}")
# ============================================================
# 5. DON'T OVER-ENCAPSULATE
# ============================================================
print("\n5. DON'T OVER-ENCAPSULATE")
# ā
DO: Encapsulate when you need control
class Good:
def __init__(self, value):
self._value = value
@property
def value(self):
return self._value
# ā DON'T: Create properties for everything
class OverEncapsulated:
def __init__(self, x, y, z):
self._x = x
self._y = y
self._z = z
@property
def x(self):
return self._x
@x.setter
def x(self, value):
self._x = value
@property
def y(self):
return self._y
@y.setter
def y(self, value):
self._y = value
@property
def z(self):
return self._z
@z.setter
def z(self, value):
self._z = value
# This is unnecessary ā just use public attributes!
# ============================================================
# 6. SUMMARY
# ============================================================
print("\n" + "=" * 60)
print("ENCAPSULATION BEST PRACTICES SUMMARY")
print("=" * 60)
print("""
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā PRACTICE ā WHY IT MATTERS ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¼āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā Use @property ā Clean, Pythonic way to control access ā
ā ā ā
ā Make attributes private ā Protects internal state ā
ā when possible ā ā
ā ā ā
ā Validate in setters ā Ensures data integrity ā
ā ā ā
ā Use read-only properties ā Expose computed values safely ā
ā for computed values ā ā
ā ā ā
ā Don't over-encapsulate ā Simplicity is better than complexity ā
ā ā ā
ā Use public attributes ā Sometimes simplicity is the best choice ā
ā when no control needed ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā“āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
š REMEMBER:
⢠Encapsulation is about controlling access, not hiding everything
⢠Use the simplest approach that meets your needs
⢠@property is the Pythonic way to encapsulate
⢠Don't write getters and setters for every attribute
""")
Best practices summary:
- Use @property ā it's the Pythonic way to implement encapsulation
- Make attributes private ā use protected/private conventions when needed
- Validate in setters ā ensure data integrity
- Use read-only properties ā for computed values
- Don't over-encapsulate ā use public attributes when no control is needed
- Keep it simple ā simplicity is a core Python principle
Quick Check: What's the Pythonic way to implement getters and setters? (Answer: Use the @property decorator)
Try It Yourself
Experiment with encapsulation in the editor below.
ENCAPSULATION - PRACTICE
==================================================
1. BASIC ENCAPSULATION
Name: Alice
Salary: $50,000
Performance: average
Bonus: $2,500
2. SETTING VALUES WITH VALIDATION
Updated salary: $60,000
Updated bonus: $12,000
ā Salary cannot be negative
ā Performance must be one of: ['excellent', 'good', 'average', 'below average', 'poor']
2. READ-ONLY PROPERTIES
Radius: 5
Area: 78.54
Circumference: 31.42
ā Cannot set area: can't set attribute
You've Got It!
You now understand encapsulation in Python. You know how to use access modifiers, getters and setters, and the @property decorator to protect your data and create clean interfaces.
Quick Quiz
Test what you've learned:
Frequently Asked Questions
What is encapsulation in Python?
What's the difference between protected and private in Python?
_name) is a convention that says "this is for internal use." It's accessible from outside but you shouldn't use it. Private (__name) uses name mangling and is harder to access from outside. Private is for implementation details you really want to hide.
Should I always use @property for attributes?
How do I create a read-only property?
@property def name(self): return self._name with no @name.setter method.
Why doesn't Python have strict access control like Java?
What's the relationship between encapsulation and data hiding?
Where to Go From Here
Now that you understand encapsulation in Python, check out these related topics:
Public, Private and Protected
Learn more about access modifiers in Python.
Learn More āInheritance vs Composition
Learn how encapsulation relates to inheritance and composition.
Learn More āPolymorphism
Learn how encapsulation enables polymorphic behavior.
Learn More ā