Menu
Coddy logo textTech

Public, Protected, Private Mem

Part of the Object Oriented Programming section of Coddy's Python journey — lesson 28 of 64.

Python has three levels of access control for class members: public, protected, and private. These control how attributes and methods can be accessed.

Here is an example of a class with all three access levels:

class BankAccount:
    def __init__(self, owner, balance, account_id):
        self.owner = owner           # Public - accessible anywhere
        self._balance = balance      # Protected - internal use
        self.__account_id = account_id  # Private - class only
    
    def deposit(self, amount):       # Public method
        self._balance += amount
    
    def _calculate_interest(self):   # Protected method
        return self._balance * 0.02
    
    def __validate_transaction(self, amount):  # Private method
        return amount > 0 and amount <= self._balance

Access public members from anywhere:

account = BankAccount("Alice", 1000, "12345")
print(account.owner)        # Alice
account.deposit(500)        # Works fine

Access protected members (single underscore - convention only):

print(account._balance)     # 1500 - works but not recommended
result = account._calculate_interest()  # Works but not recommended
print(result)               # 30.0

Try to access private members (double underscore - name mangled):

# This won't work:
# print(account.__account_id)  # AttributeError

# But this works (name mangling):
print(account._BankAccount__account_id)  # 12345

Create a subclass to show protected vs private access:

class SavingsAccount(BankAccount):
    def show_balance(self):
        return self._balance        # Protected - accessible in subclass
    
    def show_id(self):
        # return self.__account_id  # This won't work - private
        return "Cannot access private member"
savings = SavingsAccount("Bob", 2000, "67890")
print(savings.show_balance())  # 2000
print(savings.show_id())       # Cannot access private member

Output:

Alice
30.0
12345
2000
Cannot access private member

Key Point: Public members have no prefix and are accessible anywhere. Protected members use single underscore (_) and should only be used within the class hierarchy. Private members use double underscore (__) and are name-mangled for stronger privacy. Python's access control is convention-based, not strictly enforced.

challenge icon

Challenge

Medium

In this challenge, you'll implement a BankAccount class that demonstrates proper use of public, protected, and private access levels in Python.

  • bankaccount.py - Contains the class definition with TODO comments guiding your implementation
  1. Follow the TODO comments in bankaccount.py to implement the required functionality
  2. Implement proper access levels (public, protected, private) as specified
  3. Ensure all methods handle edge cases appropriately (negative deposits, etc.)

Cheat sheet

Python has three access control levels for class members:

  • Public: No prefix, accessible anywhere
  • Protected: Single underscore (_), internal use within class hierarchy
  • Private: Double underscore (__), name-mangled for class-only access
class BankAccount:
    def __init__(self, owner, balance, account_id):
        self.owner = owner           # Public
        self._balance = balance      # Protected
        self.__account_id = account_id  # Private
    
    def deposit(self, amount):       # Public method
        self._balance += amount
    
    def _calculate_interest(self):   # Protected method
        return self._balance * 0.02
    
    def __validate_transaction(self, amount):  # Private method
        return amount > 0 and amount <= self._balance

Accessing members:

account = BankAccount("Alice", 1000, "12345")

# Public access
print(account.owner)        # Works fine

# Protected access (works but not recommended)
print(account._balance)     # 1500

# Private access (name mangled)
print(account._BankAccount__account_id)  # 12345

In subclasses:

class SavingsAccount(BankAccount):
    def show_balance(self):
        return self._balance        # Protected - accessible
    
    def show_id(self):
        # return self.__account_id  # Private - not accessible
        return "Cannot access private member"

Try it yourself

from bank_account import BankAccount

# Comprehensive test case handler
test_case = input()

if test_case == "basic_test":
    # Basic functionality test
    account = BankAccount("John Doe", "12345")
    print(account.account_holder)  # Public attribute
    print(account.deposit(100))    # Public method
    print(account.get_balance())   # Public method

elif test_case == "transaction_count":
    # Testing protected attribute
    account = BankAccount("Jane Smith", "67890")
    account.deposit(50)
    account.deposit(100)
    account.deposit(200)
    print(account._transaction_count)  # Protected attribute
    print(account.get_balance())

elif test_case == "invalid_deposit":
    # Testing validation in deposit method
    account = BankAccount("Bob Johnson", "54321")
    print(account.deposit(-50))  # Should return None
    print(account.deposit(0))    # Should return None
    print(account.deposit(75))   # Should return 75
    print(account.get_balance())

elif test_case == "private_access":
    # Testing private attribute access
    account = BankAccount("Alice Brown", "98765")
    try:
        print(account.__balance)  # This will raise an AttributeError
    except AttributeError:
        pass
    
    try:
        print(account.__account_number)  # This will raise an AttributeError
    except AttributeError:
        pass
    
    print("Private attributes are not directly accessible")
    account.deposit(500)
    print(account.get_balance())  # Access through public method

elif test_case == "name_mangling":
    # Demonstrating name mangling for private attributes
    account = BankAccount("Charlie Green", "13579")
    print(account._BankAccount__balance)        # Accessing private attribute through name mangling
    print(account._BankAccount__account_number)  # Accessing private attribute through name mangling
    account.deposit(300)
    print(account._BankAccount__balance)  # Should show updated balance

elif test_case == "multiple_accounts":
    # Testing multiple accounts
    account1 = BankAccount("David White", "24680")
    account2 = BankAccount("Eva Black", "86420")
    account3 = BankAccount("Frank Red", "97531")
    
    account1.deposit(150)
    account2.deposit(250)
    account2.deposit(50)
    account3.deposit(500)
    account3.deposit(300)
    account3.deposit(200)
    
    print(f"{account1.account_holder}: {account1.get_balance()}")
    print(f"{account2.account_holder}: {account2.get_balance()}")
    print(f"{account3.account_holder}: {account3.get_balance()}")
    
    print(f"Account 1 transactions: {account1._transaction_count}")
    print(f"Account 2 transactions: {account2._transaction_count}")
    print(f"Account 3 transactions: {account3._transaction_count}")

elif test_case == "stress_test":
    # Performance test with many transactions
    account = BankAccount("Stress Tester", "11111")
    for _ in range(1000):
        account.deposit(1)
    print(f"Final balance: {account.get_balance()}")
    print(f"Transaction count: {account._transaction_count}")
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Object Oriented Programming