Previous | Tutorial index | Next

πŸ“˜ TUTORIAL 5: PUBLIC, PROTECTED, AND PRIVATE MEMBERS

Learning Objective

Understand Python’s naming conventions for controlling access to class members.

5.1 Encapsulation Revisited: Why Control Access?

In Tutorial 1, we learned that encapsulation is one of the four pillars of OOP. It involves two key concepts:

  1. Bundling – grouping data (attributes) and methods (behaviours) into a single unit (the class).
  2. Data Hiding – restricting direct access to an object's internal state to prevent accidental or malicious misuse.

In many OOP languages (like Java or C++), access modifiers like private, protected, and public are strictly enforced by the compiler or runtime. Python, however, takes a different approach: "We are all consenting adults." Python trusts programmers to respect conventions rather than enforcing strict barriers.

The Python Philosophy

Key Takeaway: In Python, encapsulation is a social contract rather than a fortress wall. You should respect the conventions, but you are not physically prevented from bypassing them.

5.2 The Three Visibility Levels in Python

1. Public Members (No Underscore)

class Person: def __init__(self, name): self.name = name # Public attribute def greet(self): # Public method return f"Hello, {self.name}!" p = Person("Alice") print(p.name) # Accessible from outside print(p.greet()) # Accessible from outside

2. Protected Members (Single Leading Underscore _)

class Person: def __init__(self, name, age): self.name = name # Public self._age = age # Protected – for use in subclasses def _validate_age(self): # Protected method return 0 <= self._age <= 150 class Employee(Person): def __init__(self, name, age, employee_id): super().__init__(name, age) self.employee_id = employee_id def display_age(self): # Accessing _age from subclass is acceptable (protected) return f"Age: {self._age}" e = Employee("Bob", 30, "E123") print(e._age) # Works, but violates convention (outside the class hierarchy) print(e._validate_age()) # Works, but should not be called from outside

3. Private Members (Double Leading Underscore __)

class BankAccount: def __init__(self, owner, balance): self.owner = owner # Public self._branch = "Main" # Protected self.__balance = balance # Private – name mangled def deposit(self, amount): if amount > 0: self.__balance += amount def get_balance(self): return self.__balance # Accessible inside the class account = BankAccount("Alice", 1000) print(account.owner) # Public – OK print(account._branch) # Protected – works but not recommended # print(account.__balance) # AttributeError: 'BankAccount' object has no attribute '__balance' print(account._BankAccount__balance) # 1000 – name mangling makes it accessible (but DON'T do this!)

5.3 Name Mangling in Detail

Name mangling is Python's way of making private members "more hidden." When you define an attribute with two leading underscores (and at most one trailing underscore), Python automatically transforms the name by prefixing it with _ClassName.

How Name Mangling Works

class Example: def __init__(self): self.__secret = 42 obj = Example() print(dir(obj)) # Output includes: '_Example__secret' (not '__secret')

Why Name Mangling? The Subclass Safeguard

class Parent: def __init__(self): self.__private = "parent's secret" class Child(Parent): def __init__(self): super().__init__() self.__private = "child's secret" # This creates a *different* attribute! c = Child() print(c._Parent__private) # "parent's secret" print(c._Child__private) # "child's secret"

Without name mangling, the child's assignment would accidentally overwrite the parent's attribute. With mangling, each class has its own private namespace, avoiding accidental collisions. This is the primary purpose of name mangling.

Important: Name Mangling and Method Names

The same mangling applies to method names:

class Secret: def __secret_method(self): return "secret" s = Secret() # s.__secret_method() # AttributeError s._Secret__secret_method() # Works, but don't do this.

5.4 Common Misconceptions About "Private" in Python

Misconception Reality
"Private attributes are completely inaccessible." False – they are accessible via name mangling (_ClassName__attribute).
"Protected attributes are automatically inherited." They are inherited just like public attributes, but the convention says they should be used with care in subclasses.
"Name mangling makes the attribute impossible to override in subclasses." False – subclasses can define their own __attribute, which will be mangled to _Subclass__attribute, so they are separate.
"Python enforces access restrictions like Java." False – Python uses conventions; it does not enforce them at the language level.

5.5 Best Practices: When to Use Each Level

Visibility When to Use
Public For the class's main interface – attributes and methods that external code should use freely. This includes most methods and some attributes (e.g., name, id).
Protected (_) For attributes and methods that are part of the internal implementation but may be needed by subclasses. Use it for helper methods, internal state that subclasses might want to access or override.
Private (__) For attributes and methods that are implementation details that should never be accessed or overridden by subclasses. Use it to prevent name collisions and to signal "this is absolutely internal." However, many Python developers prefer using _ for most cases and reserve __ only when name collisions are a real concern.

Recommendation from the Python Community

5.6 Accessing "Private" Attributes – Why You Shouldn't

Although it's technically possible to access mangled names, you should never do so in production code. Here's why:

  1. It violates the class's contract – the author intended that attribute to be hidden.
  2. It breaks encapsulation – changes to the internal implementation (e.g., renaming the attribute) will break your code.
  3. It's brittle – the mangled name includes the class name, so if you rename the class, your external code breaks.
  4. It confuses other developers – they will not expect external code to depend on mangled names.

Example of Why Not to Do It

class DatabaseConnection: def __init__(self, url): self.__url = url def connect(self): # Use __url internally pass # Some external code: db = DatabaseConnection("postgres://...") # BAD IDEA: db._DatabaseConnection__url = "hacked!" # Breaks everything!

If the library author later renames __url to __connection_string, your hack breaks. Always use the public interface.

5.7 Property Decorators – A Better Way to Control Access

In Tutorials 10 and 11, you'll learn about @property decorators, which allow you to define controlled access to attributes. Properties let you expose an attribute publicly while hiding the internal storage and adding validation logic.

For now, here's a sneak peek:

class Person: def __init__(self, name, age): self.name = name self._age = age # Protected @property def age(self): """Getter for age.""" return self._age @age.setter def age(self, value): """Setter with validation.""" if value < 0: raise ValueError("Age cannot be negative") self._age = value p = Person("Alice", 30) print(p.age) # Uses the getter p.age = 35 # Uses the setter (with validation) # p._age = 40 # Bypasses validation – not recommended

Properties allow you to maintain encapsulation while providing a clean public interface.

5.8 Summary of Underscore Naming Patterns

Pattern Meaning Example
name Public – part of the official interface. self.name
_name Protected – internal use, but may be accessed in subclasses. self._age
__name Private – name mangled; avoid external access. self.__ssn
__name__ Dunder (magic) methods – reserved for Python's special methods. Do not invent your own. __init__, __str__
name_ Used to avoid name conflicts with keywords. class_ (to avoid conflict with class)

πŸ“ Quiz 5: Access Modifiers

Answer the following questions to check your understanding.

1. How do you denote a protected attribute in Python?

Answer(B) `self._attribute`

2. What does Python's name mangling do to self.__secret in a class named MyClass?

Answer(B) It changes it to `self._MyClass__secret`

3. (True/False) Python strictly enforces access restrictions – attempting to access a private attribute from outside the class will raise an error.

Answer(B) False – Python uses conventions, not enforcement.

4. Which of the following is the primary purpose of name mangling?

Answer(B) To prevent accidental name clashes in subclasses.

5. Consider the following code:

class Test: def __init__(self): self._x = 10 self.__y = 20 t = Test() print(t._x) print(t.__y)

What happens?

Answer(B) Prints 10, then raises AttributeError because `__y` is mangled to `_Test__y`.

6. Which of the following is a valid way to access the private attribute __balance of class Account from outside the class?

Answer(B) `account._Account__balance`

7. What is the convention for using a single leading underscore _?

Answer(C) It indicates that the attribute is intended for internal use, and subclasses may use it.

8. Why is it considered bad practice to access a mangled attribute like _ClassName__attr from outside the class?

Answer(B) It violates encapsulation and may break if the class changes.

πŸ§ͺ Exercise 5: Implementing Access Control

Part A: Design a Class with All Three Levels
Create a Customer class with:

Add methods:

Instantiate a Customer object and demonstrate:

Part B: Subclass Behaviour
Create a subclass VIPCustomer that inherits from Customer. Add a new private attribute __vip_level (int). In VIPCustomer, try to access the parent's __password_hash directly – what happens? Then access the parent's _email (protected) – that should work fine.

Part C: Reflection
In comments, answer: "Why might the designer of Customer choose to make _email protected rather than public? What are the benefits?"

Sample Answer Making `_email` protected instead of public signals that the email address is an internal piece of data that should not be modified arbitrarily from outside the class. The class provides a public method (`update_email`) to change the email, which can include validation logic (e.g., checking format). This ensures that the email format is always valid and encapsulates the logic of updating, making the class more robust and easier to maintain.

🏠 Homework 5: Building a Secure User Management System

Task: You are building a user management system for a web application. Create a complete Python script that models User and AdminUser classes with appropriate access modifiers. Follow the specifications below.

Class: User

Attributes:

Methods:

Subclass: AdminUser (inherits from User)

Additional Attributes:

Additional Methods:

Important: Override display_info()? No, just use the inherited one and add the admin level in the new method.

Homework Submission Requirements

Part 1: Code
Write the complete class definitions with all methods, including proper use of super() and appropriate access modifiers. Add docstrings.

Part 2: Testing Script
In the same file, write a test function that does the following:

  1. Create a regular User ("alice", "alice@example.com", "pass123").
  2. Create an AdminUser ("bob", "bob@admin.com", "adminpass", admin_level=5).
  3. Display user info using display_info().
  4. Attempt to change Alice's password with the wrong old password – should fail.
  5. Change Alice's password with the correct old password – should succeed.
  6. Update Alice's email to a valid one – should succeed; try an invalid one – should fail.
  7. Display the admin user's info using display_admin_info().
  8. Promote the admin user and display again.
  9. Attempt to access __password_hash directly from both user objects – show that it raises an AttributeError. Then, for demonstration purposes, access the mangled name _User__password_hash and print its value (include a comment explaining why this is bad practice).

Part 3: Written Reflection
Answer these questions in a comment block at the top of your script:

  1. Why did we make __password_hash private? What could happen if it were public?
  2. Why did we make _email protected rather than private? (Hint: think about the possibility of a subclass needing to validate email differently.)
  3. The _hash_password() method is protected. Why not make it private?
  4. If a developer working on the same project decides to access user._User__password_hash and modify it directly, what could go wrong?
  5. In your opinion, is Python's approach to access control (conventions over enforcement) a strength or a weakness? Justify your answer.
Sample Answers (Part 3) 1. Making `__password_hash` private ensures that the password hash cannot be read or modified directly from outside the class. If it were public, any part of the program could change it, allowing an attacker to bypass password authentication or corrupt the user's credentials. 2. `_email` is protected because it may need to be accessed or overridden by subclasses. For example, a `PremiumUser` subclass might want to validate email against a different domain. Making it private would prevent such extension. 3. `_hash_password()` is protected because subclasses might want to use it for their own password hashing logic or extend it (e.g., adding salt). Making it private would restrict that flexibility. 4. If a developer directly modifies `user._User__password_hash`, they could set it to an arbitrary value, effectively breaking the password validation mechanism. The user might lose access, or an attacker could take over the account. 5. Python's approach is a strength because it respects the developer's responsibility and keeps the language flexible. It encourages good practices through conventions rather than rigid enforcement, which can be restrictive in certain dynamic scenarios. However, it also requires discipline to follow the conventions.

πŸ“š Additional Resources for Self‑Study

  1. Real Python: Single and Double Underscore Naming Conventions in Python – Excellent deep dive.
  2. Educative: What are public, protected, & private access modifiers in Python? – Clear summary.
  3. Python Official Docs: 9. Classes – Private Variables – The authoritative explanation.
  4. Stack Overflow: Why use double underscore? – Community discussion.

βœ… Summary Checklist for Tutorial 5

Before moving to Tutorial 6 (Class Attributes), ensure you can confidently say YES to the following:

Ready for the next tutorial? In Tutorial 6, you will learn about class attributes – attributes that are shared by all instances of a class, and how they differ from instance attributes.

Previous | Tutorial index | Next