How to Enforce Attribute Privacy in Python
Python's philosophy on data privacy is distinct from languages like Java or C++. Instead of strict private or protected keywords, Python relies on naming conventions and encapsulation mechanisms to signal intent and control access. The mantra is "We are all consenting adults here," meaning privacy is advisory rather than mandatory.
This guide explains how to use naming conventions, property decorators, and __slots__ to enforce attribute privacy effectively.
Understanding Python's Privacy Model
In Python, there is no way to make an attribute strictly inaccessible from outside the class. However, you can make it harder to access accidentally.
- Public (
name): Accessible everywhere. - Protected (
_name): Convention only. Signals "internal use." - Private (
__name): Name mangling applied. Harder to access accidentally.
Method 1: Protected Attributes (Single Underscore)
Prefixing an attribute with a single underscore _ signals to other developers (and IDEs/linters) that it is intended for internal use only. It does not prevent access technically.
class DatabaseConnection:
def __init__(self):
self.host = "localhost" # Public
self._password = "secret123" # Protected (Convention)
db = DatabaseConnection()
# ✅ Public access is normal
print(db.host)
# ⚠️ Warning: Accessing protected member (Discouraged but works)
print(db._password)
Output:
localhost
secret123
Use this for attributes that are internal implementation details but might be needed by subclasses.
Method 2: Private Attributes (Double Underscore / Mangling)
Prefixing an attribute with two underscores __ (and no trailing underscores) triggers Name Mangling. Python internally renames the attribute to _ClassName__attributeName. This prevents accidental collisions in subclasses and makes direct access harder.
class BankAccount:
def __init__(self, balance):
self.__balance = balance # Private
def get_balance(self):
return self.__balance
account = BankAccount(1000)
# ✅ Access via method works
print(f"Balance: {account.get_balance()}")
try:
# ⛔️ Error: Attribute appears to not exist
print(account.__balance)
except AttributeError as e:
print(f"Error: {e}")
# ⚠️ Technically accessible via mangled name (Not recommended)
print(f"Mangled Access: {account._BankAccount__balance}")
Output:
Balance: 1000
Error: 'BankAccount' object has no attribute '__balance'
Mangled Access: 1000
Method 3: Using Property Decorators (Encapsulation)
The most robust way to "enforce" privacy logic (like read-only access or validation) is using properties. You store the data in a protected/private attribute but expose it via @property.
class User:
def __init__(self, username):
self._username = username # Internal storage
# ✅ Getter: Allows reading
@property
def username(self):
return self._username
# ✅ Setter: Allows writing with validation
@username.setter
def username(self, new_name):
if not new_name:
raise ValueError("Username cannot be empty")
self._username = new_name
user = User("Alice")
# Read
print(user.username)
# Write (Triggering validation)
try:
user.username = ""
except ValueError as e:
print(f"Error: {e}")
Output:
Alice
Error: Username cannot be empty
Conclusion
To enforce attribute privacy in Python:
- Use
_attributefor internal implementation details (Standard Convention). - Use
__attributeonly to prevent name collisions in inheritance scenarios (Name Mangling). - Use
@propertyto control access logic (Read-only, Validation) while keeping a clean public API.