Python Access Modifiers: Public, Protected, Private
In Object-Oriented Programming, encapsulation is the principle of bundling data (attributes) and methods that operate on that data into a single unit and controlling access to that data. Python's approach is unique: it enforces access control through naming conventions rather than strict keywords. Understanding public, protected, and private members is essential for writing secure, maintainable classes that prevent accidental data corruption.
Prerequisites
You should be comfortable defining classes with attributes and methods, and understand basic OOP principles.
Public Members: The Default (No Underscores)
By default, all attributes and methods in a Python class are public—accessible from anywhere. Public members signal to developers that a component is intended for external use.
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner # Public attribute
self.balance = balance # Public attribute
def deposit(self, amount): # Public method
self.balance += amount
print(f"Deposited ${amount}. New balance: ${self.balance}")
# Access public members from outside the class
account = BankAccount("Alice", 1000)
print(account.owner) # Output: Alice
print(account.balance) # Output: 1000
account.balance = -500 # Directly modify—dangerous!
print(account.balance) # Output: -500
This demonstrates the risk: without any restrictions, external code can set the balance to an invalid value (negative money). Public members should only be used for data that's safe to modify directly.
Protected Members: Single Underscore Convention
To signal that an attribute or method should not be accessed from outside the class (but may be accessed by subclasses), prefix it with a single underscore (_).
This is purely a convention. Python does not technically prevent access—it's a "gentleman's agreement" among developers that says, "You can technically access this, but you shouldn't." Protected members are used for internal helper methods or attributes that subclasses might need.
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner
self._balance = balance # Protected attribute
def deposit(self, amount):
if amount > 0:
self._balance += amount
self._log_transaction(f"Deposited ${amount}")
else:
print("Deposit amount must be positive.")
def _log_transaction(self, message): # Protected method
"""Internal logging—not for public use."""
print(f"[LOG] {message}")
# Access protected members (not recommended, but possible)
account = BankAccount("Bob", 2000)
account.deposit(500)
print(account._balance) # Output: 2500 (works, but shouldn't)
# A subclass can use protected members
class SavingsAccount(BankAccount):
def apply_interest(self, rate):
# Subclass can access protected _balance
interest = self._balance * rate
self._balance += interest
self._log_transaction(f"Applied {rate*100}% interest")
Protected members are intended for inheritance: a subclass might override or extend protected methods. External code should use public methods instead.
Private Members: Double Underscore and Name Mangling
To strongly discourage access to an attribute or method from outside the class, prefix it with a double underscore (__). Python applies a mechanism called name mangling: it automatically renames the attribute by prepending the class name.
How Name Mangling Works
When Python sees __attribute, it internally changes it to _ClassName__attribute. This makes it difficult (not impossible, but impractical) to access the attribute directly by its original name.
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner
self.__balance = balance # Private attribute
def deposit(self, amount):
if amount > 0:
self.__balance += amount
self.__log_transaction(f"Deposited ${amount}")
else:
print("Deposit amount must be positive.")
def get_balance(self):
"""The only public way to access the balance."""
return self.__balance
def __log_transaction(self, message): # Private method
print(f"[PRIVATE LOG] {message}")
# Try to access private member directly
account = BankAccount("Charlie", 5000)
account.deposit(1000)
# This works—public method
print(f"Balance via method: ${account.get_balance()}") # Output: $6000
# Attempting to access by original name fails
try:
print(account.__balance)
except AttributeError as e:
print(f"Error: {e}")
# Output: Error: 'BankAccount' object has no attribute '__balance'
# You CAN access it using the mangled name (but this is bad practice)
print(f"Via name mangling: ${account._BankAccount__balance}")
# Output: Via name mangling: $6000
Output:
[PRIVATE LOG] Deposited $1000
Balance via method: $6000
Error: 'BankAccount' object has no attribute '__balance'
Via name mangling: $6000
Name mangling prevents accidental access but doesn't prevent intentional circumvention. Its real purpose is to prevent name conflicts in inheritance: if a parent class and child class both have a __balance, they won't collide (they become _Parent__balance and _Child__balance).
Why Access Control Matters: Encapsulation in Practice
Encapsulation protects data integrity. Consider a Temperature class:
class Temperature:
def __init__(self, celsius):
self.__celsius = celsius # Private
def set_celsius(self, value):
# Validate before setting
if value < -273.15:
print("Error: Temperature cannot be below absolute zero.")
else:
self.__celsius = value
print(f"Temperature set to {value}°C")
def get_celsius(self):
return self.__celsius
def get_fahrenheit(self):
return (self.__celsius * 9/5) + 32
# Use controlled methods
temp = Temperature(25)
print(f"Celsius: {temp.get_celsius()}") # Output: 25
print(f"Fahrenheit: {temp.get_fahrenheit()}") # Output: 77.0
# Attempt invalid temperature
temp.set_celsius(-300)
# Output: Error: Temperature cannot be below absolute zero.
By using set_celsius() instead of direct assignment, we validate input. If __celsius were public, nothing would prevent invalid values.
Public, Protected, Private: Quick Reference
| Level | Syntax | Usage | Access Control |
|---|---|---|---|
| Public | name | External use; part of class contract | None—use freely |
| Protected | _name | Subclass use; internal helpers | Convention only; not enforced |
| Private | __name | Class-internal only | Name mangling; prevented by naming |
Key Takeaways
- Public members (no underscores) are freely accessible and should be safe to modify.
- Protected members (single underscore
_name) signal "internal use; don't access from outside," but Python doesn't enforce this—it's a convention. - Private members (double underscore
__name) are renamed by name mangling to_ClassName__name, preventing direct access by original name and enforcing stronger encapsulation. - Name mangling protects against accidental access and prevents name conflicts in inheritance but doesn't prevent intentional circumvention.
- Use private members for data that must be validated before modification; provide public methods for safe access and modification.
- Encapsulation via access control ensures data integrity, prevents bugs, and makes code more maintainable.
Frequently Asked Questions
Is there a way to make a member truly private and completely inaccessible?
No. Python's philosophy is "we're all consenting adults." Name mangling prevents accidental access, but someone who knows the mangled name (_ClassName__attr) can still access it. This is intentional—Python trusts developers to respect conventions. For truly sensitive data, use external libraries like cryptography to encrypt values, but for normal class design, name mangling is sufficient.
Should I make all attributes private and use getter/setter methods?
Not necessarily. If an attribute is safe to modify directly (e.g., a person's name), make it public. Use private attributes only when modification needs validation or has side effects. The goal is clarity and safety, not maximum privacy. Excessive getter/setter methods add noise without benefit.
Can a subclass access protected and private members?
A subclass can access protected members (_name)—that's their purpose. Private members (__name) are technically inaccessible by their original name but accessible via the mangled name. However, relying on private members from a subclass is fragile; the parent class can change the mangling without warning. In practice, use protected members for inheritance; reserve private for true internal state.
What's the difference between _name (single underscore) and __name__ (double underscores on both sides)?
Single underscore _name is a convention for protected members. Double underscores on both sides __name__ (like __init__, __str__) are special Python methods (dunder methods) with built-in meaning. They're not access-control related; they're magic methods that Python calls implicitly (e.g., __init__ on object creation).
Can I access a private member from another instance of the same class?
Yes. Name mangling is class-wide, not instance-specific:
class Account:
def __init__(self, balance):
self.__balance = balance
def compare_balance(self, other):
# Can access other's private balance
return self.__balance > other.__balance
a1 = Account(1000)
a2 = Account(500)
print(a1.compare_balance(a2)) # Output: True
Private is about external access, not internal class logic.