Skip to main content

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

LevelSyntaxUsageAccess Control
PublicnameExternal use; part of class contractNone—use freely
Protected_nameSubclass use; internal helpersConvention only; not enforced
Private__nameClass-internal onlyName 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.


Further Reading