Previous | Tutorial index | Next
Understand Pythonβs naming conventions for controlling access to class members.
In Tutorial 1, we learned that encapsulation is one of the four pillars of OOP. It involves two key concepts:
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.
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.
name, age, calculate_salary().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
_)_age, _internal_value._name means "protected β use with caution outside the class hierarchy."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
__)__ssn, __balance._ClassName__attribute internally.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!)
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.
class Example:
def __init__(self):
self.__secret = 42
obj = Example()
print(dir(obj))
# Output includes: '_Example__secret' (not '__secret')
__secret is changed to _Example__secret.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.
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.
| 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. |
| 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. |
__ (double underscore) for data hiding. They rely on _ (single underscore) to signal "internal use," and they document the class thoroughly.__ primarily when you are writing a library or framework where name clashes in subclasses are a real risk._ is sufficient.Although it's technically possible to access mangled names, you should never do so in production code. Here's why:
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.
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.
| 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) |
Answer the following questions to check your understanding.
1. How do you denote a protected attribute in Python?
self.__attributeself._attributeself.attributeself.#attribute2. What does Python's name mangling do to self.__secret in a class named MyClass?
self._secretself._MyClass__secretself.__secret (no change)3. (True/False) Python strictly enforces access restrictions β attempting to access a private attribute from outside the class will raise an error.
4. Which of the following is the primary purpose of name mangling?
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?
_Test__y (mangled)6. Which of the following is a valid way to access the private attribute __balance of class Account from outside the class?
account.__balanceaccount._Account__balanceaccount.__balance()7. What is the convention for using a single leading underscore _?
8. Why is it considered bad practice to access a mangled attribute like _ClassName__attr from outside the class?
Part A: Design a Class with All Three Levels
Create a Customer class with:
name (str)._email (str).__password_hash (str, initialized to a dummy value, e.g., "hash123").Add methods:
update_email(new_email) β updates _email (with some validation, e.g., must contain '@')._hash_password(password) β returns a string (simulate hashing by returning "hashed_" + password).__validate_credentials(input_password) β returns True if the hashed input matches __password_hash (for simplicity, just compare the hashed values).Instantiate a Customer object and demonstrate:
name (public) β OK._email directly β works but violates convention.__password_hash β will fail (explain why).update_email() β works._hash_password() from outside β works but is not recommended.__password_hash using _Customer__password_hash β show that it's possible but discouraged.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?"
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.
UserAttributes:
username (str)_email (str)__password_hash (str) β store a hashed password (simulate by using hashlib.sha256 or a simple string, e.g., "hashed_" + password).Methods:
__init__(username, email, password) β initialise all attributes; hash the password using _hash_password().change_password(old_password, new_password) β if __validate_password(old_password) returns True, update __password_hash with the new password's hash and return True; otherwise return False.update_email(new_email) β if the new email contains '@', update _email and return True; otherwise return False._hash_password(password) β returns a hash string (e.g., "hashed_" + password).__validate_password(input_password) β returns True if _hash_password(input_password) == __password_hash; otherwise False.display_info() β returns a string like "Username: alice, Email: alice@example.com" (do not expose the password hash).AdminUser (inherits from User)Additional Attributes:
__admin_level (int, default 1).Additional Methods:
promote() β increases __admin_level by 1.display_admin_info() β returns f"{display_info()} | Admin Level: {__admin_level}".Important: Override display_info()? No, just use the inherited one and add the admin level in the new method.
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:
User ("alice", "alice@example.com", "pass123").AdminUser ("bob", "bob@admin.com", "adminpass", admin_level=5).display_info().display_admin_info().__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:
__password_hash private? What could happen if it were public?_email protected rather than private? (Hint: think about the possibility of a subclass needing to validate email differently.)_hash_password() method is protected. Why not make it private?user._User__password_hash and modify it directly, what could go wrong?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.