Python

P
PythonHub
Progress 0%

Python

  • Home
  • History of Python
  • Applications of Python
  • Introduction To Python
    • What is Python
    • Character Set
    • Tokens in Python
    • Python Execution Mode
    • Variable And Identifiers
    • Data Types in Python
    • Operators And Expressions
    • Constants in Python
    • Assignment Statement
    • Input / Output in Python
    • Simple Python Scripts
    • Namespace in Python
    • šŸ“ Assignments
  • Operators in Python
    • Arithmetic Operators
    • Assignment Operators
    • Shorthand Assignment Operators
    • Relational Operators
    • Logical Operators
    • Bitwise Operators
    • Special Operators
    • šŸ“ Assignments
  • Input Output in Python
    • Accept Input
    • Output Formatting
    • šŸ“ Assignments
  • Conditional Statement
    • Decision Making
    • if Statement
    • IF-ELSE STATEMENT
    • IF-ELSE LADDER
    • NESTED IF-ELSE
    • Short Hand IF-ELSE
    • šŸ“ Assignments
  • Loops
    • Introduction to Loops
    • While Loop
    • Nested While Loop
    • šŸ“ While Loop Assignments
    • For Loop
    • For Loop Examples
    • Nested For Loop
    • Nested For Loop Examples
    • Infinite While Loops
    • Infinite For Loops
    • Break, Continue and Else in Loops
    • Difference Between For and While Loop
    • For Each Loop
    • šŸ“ For Each Assignments
    • šŸ“ All Loops Assignments
  • List
    • List in Python
    • Access List Elements
    • List Functions
    • Iterate (Loop) List
    • List Comprehension
    • šŸ“ Assignments
  • Tuple
    • Tuple in Python
    • Access Tuple Elements
    • Tuple Functions
    • Iterate (Loop) Tuple
    • Unpack Tuple
    • Tuple Comprehension
    • šŸ“ Assignments
  • Set
    • Set in Python
    • Access Set Elements
    • Set Methods
    • Iterate (Loop) Set
    • Pack/Unpack Set
    • Set Comprehension
    • šŸ“ Assignments
  • Dictionary
    • Dictionary
    • Access Dictionary Items
    • Dictionary Methods
    • Iterate (Loop) Dictionary
    • Formatting Dictionaries
    • Nested Dictionaries
    • Dictionary Comprehension
    • šŸ“ Assignments
  • Diff List Tuple Set Dictionary
    • List vs Tuple
    • List vs Set
    • List vs Dictionary
    • Tuple vs Set
    • Tuple vs Dictionary
    • Dictionary vs Set
    • šŸ“ Assignments
  • Exception
    • Error vs Exception
    • Types of Exception
    • Exception Handling
    • User Defined Exception
    • Logging Exception
    • šŸ“ Assignments
  • Functions
    • Introduction to Functions
    • Modular Programming
    • Types of Functions
    • Inbuilt Functions
    • Need For User-Defined Function
    • Elements of User Defined Function
    • Function Arguments
    • Nesting of Functions
    • Recursion
    • Global Local and Non Local
    • Python Lambda Functions
    • šŸ“ Assignments
  • Python Module
    • Introduction to Module
    • Inbuilt Modules in Python
    • User Defined Module
    • šŸ“ Assignments
  • File Handling
    • Introduction to Files
    • Create File
    • Read Files
    • Write to File
    • Rename File
    • Copy File
    • Move File
    • List Files in Directory
    • Binary Files
    • Zipping and Unzipping Files
    • šŸ“ Assignments
  • Strings
    • Basics of Strings
    • String Special Operators
    • String Formatting Operators
    • String Methods
    • šŸ“ Assignments
  • Regular Expressions
  • Python OOPS
    • Basics of Object Oriented
    • What are Classes and Objects?
    • Creating Class and Object
    • OOP vs Procedural Programming
    • Difference Between Classes and Objects
    • Constructors
    • Destructor
    • Built Class Methods and Attributes
    • Class and Instance Variables
    • Inheritance in Python
    • Single Inheritance
    • Multiple Inheritance
    • Multilevel Inheritance
    • Hierarchical Inheritance
    • Hybrid Inheritance
    • Abstraction
    • Method Overriding
    • Abstract Method
    • Interfaces in Python
    • Abstract Class vs Interface
    • Public, Private and Protected
    • Overloading vs Overriding
    • Inheritance vs Composition
    • Encapsulation
    • Polymorphism
    • Inner Classes
    • šŸ“ Assignments
  • Advanced Python
    • Iterator in Python
    • Generator in Python
    • Decorator in Python
    • Type Hints in Python
    • Async/Await Programming
    • Dataclasses in Python
    • Context Managers in Python
    • Property Decorator in Python
    • __slots__ in Python
    • Enums in Python
    • Walrus Operator in Python
    • Match-Case in Python
    • šŸ“ Assignments
  • Python Standard Library
    • Collections Module
    • Itertools Module
    • Functools Module
    • Datetime Module
    • JSON Module
    • OS Module
    • Sys Module
    • Random Module
    • Math Module
    • šŸ“ Assignments
  • Python Testing
    • Unit Testing in Python
    • Pytest Framework
    • Mocking in Python
    • šŸ“ Assignments
  • Python Best Practices
    • PEP 8 Style Guide
    • Docstrings in Python
    • Logging in Python
    • Code Optimization Tips
    • Debugging Techniques
    • šŸ“ Assignments
  • MySQL Database in Python
    • Introduction to MySQL with Python
    • DBMS vs File System
    • Connecting to MySQL Database
    • Create Database in MySQL
    • Create Table in MySQL
    • Insert Data in MySQL
    • Insert Multiple Rows
    • Select Data from MySQL
    • WHERE Clause in MySQL
    • Update Data in MySQL
    • Delete Data from MySQL
    • Parameterized Queries
    • Transaction Management
    • Error Handling
    • Connection Pooling
    • MySQL Drivers Guide
    • Joins in MySQL
    • Aggregation Functions
    • Backup MySQL Database
    • Best Practices
    • šŸ“ Assignments
  • MySQL Database Operations
    • SELECT Statement
    • MySQL Operators
    • DDL Statements
    • DML Statements
    • Subqueries
    • JOIN Operations
    • Aggregation
    • Case Study
    • šŸ“ Assignments
  • Graphics in Python
  • Threads in Python
    • Introduction to Threads
    • Process vs Threads
    • Concurrent Programming & GIL
    • Uses of Threads
    • Creating Threads
    • Single Tasking
    • Multi Tasking
    • Thread Synchronization
    • šŸ“ Assignments
  • Interview Questions & Answers
  • Python Case Studies
  • Multiple Choice Questions
  • šŸ“ Practice Problems
Get in Touch
  • tech2dsm@gmail.com

© Sankalan Data Tech

Python Language Interactive Tutorial

Python: Public, Private and Protected

Python Public Private Protected - Access Modifiers Explained

Learn about access modifiers and encapsulation in Python.

Created by Sankalan Data Tech Team Verified
Data Engineers, Analysts, Scientists & Trainers
Created by experienced Python developers, data engineers, and data scientists to make programming easy through practical examples, real-world experience, and clear explanations.
On this page:
  • What are Access Modifiers?
  • Public Members
  • Protected Members
  • Private Members
  • Name Mangling Explained
  • Comparison Table
  • Real-World Example
  • Best Practices
  • Try It Yourself
  • Quick Quiz
  • Frequently Asked Questions
Share this tutorial:
Twitter LinkedIn Facebook WhatsApp Reddit Telegram Email Copy Link
What You'll Learn Here
  • Access modifiers — controlling visibility of class members
  • Public — default, accessible from anywhere
  • Protected — intended for internal use and subclasses
  • Private — hidden from outside, with name mangling
  • Name mangling — how Python makes private members truly private
  • Encapsulation — protecting data and implementation details

What are Access Modifiers?

Access modifiers control the visibility of class attributes and methods. They help implement encapsulation — one of the core principles of Object-Oriented Programming.

Unlike languages like Java or C++ that have explicit keywords (public, private, protected), Python takes a convention-based approach:

  • Public — name (no underscore)
  • Protected — _name (single underscore)
  • Private — __name (double underscore)

Python's philosophy is: "We're all consenting adults here." The language trusts developers to respect the conventions rather than enforcing them rigidly.

šŸ’” Key concept: Python's access modifiers are conventions — they signal intent to other developers, but don't prevent access. The exception is double-underscore (__name) which triggers name mangling.

Public Members

1

The Default Visibility

In Python, everything is public by default. Public members can be accessed from anywhere — inside the class, outside the class, and in subclasses.

Public members are defined without any leading underscores. They're the "normal" way to define attributes and methods.

# Public Members — Default Visibility

class Person:
    def __init__(self, name, age):
        self.name = name      # Public attribute
        self.age = age        # Public attribute
    
    # Public method
    def introduce(self):
        return f"Hi, I'm {self.name} and I'm {self.age} years old"
    
    # Public method
    def celebrate_birthday(self):
        self.age += 1
        return f"Happy Birthday! Now {self.age} years old"

# Access from outside
person = Person("Alice", 25)

print(person.name)           # Alice — accessible
print(person.age)            # 25 — accessible

print(person.introduce())    # Hi, I'm Alice and I'm 25 years old
print(person.celebrate_birthday())  # Happy Birthday! Now 26 years old

# Subclass can also access public members
class Student(Person):
    def __init__(self, name, age, grade):
        super().__init__(name, age)
        self.grade = grade
    
    def show_info(self):
        # Accessing parent's public members
        return f"{self.name} is in grade {self.grade}"

student = Student("Bob", 18, 12)
print(student.show_info())   # Bob is in grade 12
print(student.name)          # Bob — accessible

print("\nāœ… Public members: accessible from anywhere")
print("āœ… No special syntax needed — just use the name")

Public key points:

  • Default visibility — no underscores needed
  • Accessible everywhere — inside class, outside, in subclasses
  • Convention — used for the public API of your class
  • No restrictions — Python doesn't prevent access

Quick Check: How do you define a public attribute in Python? (Answer: Just use the name without any underscores, e.g., self.name)

Protected Members

2

Single Underscore Convention

Protected members are indicated by a single leading underscore (_name). They signal: "This is intended for internal use. Don't access it from outside."

Protected members can still be accessed from outside, but the underscore is a convention telling developers: "You shouldn't touch this unless you know what you're doing."

# Protected Members — Single Underscore Convention

class BankAccount:
    def __init__(self, account_holder, balance):
        self.account_holder = account_holder  # Public
        self._balance = balance               # Protected — internal use
        self._transaction_history = []        # Protected — internal use
    
    # Public method — API for users
    def deposit(self, amount):
        if amount > 0:
            self._balance += amount
            self._add_transaction(f"Deposit: +${amount}")
            return f"Deposited ${amount}. New balance: ${self._balance}"
        return "Invalid amount"
    
    # Public method — API for users
    def withdraw(self, amount):
        if 0 < amount <= self._balance:
            self._balance -= amount
            self._add_transaction(f"Withdrawal: -${amount}")
            return f"Withdrew ${amount}. New balance: ${self._balance}"
        return "Insufficient funds or invalid amount"
    
    # Public method — API for users
    def get_balance(self):
        return self._balance
    
    # Protected method — internal helper
    def _add_transaction(self, description):
        self._transaction_history.append(description)
    
    # Protected method — internal helper
    def _get_transaction_history(self):
        return self._transaction_history

class SavingsAccount(BankAccount):
    def __init__(self, account_holder, balance, interest_rate):
        super().__init__(account_holder, balance)
        self._interest_rate = interest_rate
    
    # Can access protected members from parent
    def apply_interest(self):
        interest = self._balance * self._interest_rate / 100
        self._balance += interest
        self._add_transaction(f"Interest: +${interest:.2f}")
        return f"Interest applied: ${interest:.2f}"
    
    def get_transactions(self):
        # Accessing protected method from parent
        return self._get_transaction_history()

# --- Using the class ---
account = BankAccount("Alice", 1000)

print(account.deposit(500))    # Deposited $500. New balance: $1500
print(account.withdraw(200))   # Withdrew $200. New balance: $1300

# Protected members can be accessed, but it's discouraged
print(account._balance)        # 1300 — accessible but discouraged
print(account._transaction_history)  # accessible but discouraged

# Subclass can access protected members
savings = SavingsAccount("Bob", 1000, 2.5)
print(savings.apply_interest())  # Interest applied: $25.00
print(savings.get_transactions())

print("\nāœ… Protected members: single underscore — convention, not enforcement")
print("āš ļø They can be accessed from outside, but don't do it!")
print("āœ… Subclasses can access them freely")

Protected key points:

  • Single underscore — _name indicates protected
  • Convention only — no enforcement by Python
  • For internal use — intended for class and subclasses
  • Accessible from subclasses — designed for inheritance
  • Discouraged from outside — signals "don't touch"

Quick Check: What does a single underscore prefix (_name) indicate? (Answer: Protected — intended for internal use and subclasses, but just a convention)

Private Members

3

Double Underscore and Name Mangling

Private members are indicated by a double leading underscore (__name). Unlike protected, private members cannot be accessed from outside the class — at least not easily.

Python uses name mangling to make private members harder to access. The name is changed to _ClassName__name, which prevents accidental access.

# Private Members — Double Underscore and Name Mangling

class BankAccount:
    def __init__(self, account_holder, balance):
        self.account_holder = account_holder  # Public
        self.__balance = balance               # Private — name mangling
        self.__transaction_history = []        # Private — name mangling
        self.__account_number = self.__generate_account_number()
    
    # Private method — name mangled
    def __generate_account_number(self):
        import random
        return f"ACC-{random.randint(10000, 99999)}"
    
    # Public method — API for users
    def deposit(self, amount):
        if amount > 0:
            self.__balance += amount
            self.__add_transaction(f"Deposit: +${amount}")
            return f"Deposited ${amount}. New balance: ${self.__balance}"
        return "Invalid amount"
    
    # Public method — API for users
    def withdraw(self, amount):
        if 0 < amount <= self.__balance:
            self.__balance -= amount
            self.__add_transaction(f"Withdrawal: -${amount}")
            return f"Withdrew ${amount}. New balance: ${self.__balance}"
        return "Insufficient funds or invalid amount"
    
    # Public method — API for users
    def get_balance(self):
        return self.__balance
    
    def get_account_info(self):
        return f"{self.account_holder} - {self.__account_number}"
    
    # Private method — name mangled
    def __add_transaction(self, description):
        self.__transaction_history.append(description)
    
    # Protected method — exposes private data safely
    def _get_transactions(self):
        # Return a copy to prevent modification
        return self.__transaction_history.copy()

class SavingsAccount(BankAccount):
    def __init__(self, account_holder, balance, interest_rate):
        super().__init__(account_holder, balance)
        self._interest_rate = interest_rate
    
    def apply_interest(self):
        # Cannot access __balance directly — name mangled!
        # self.__balance += interest  # This would create a NEW attribute!
        
        # Must use the public method
        balance = self.get_balance()
        interest = balance * self._interest_rate / 100
        self.deposit(interest)
        return f"Interest applied: ${interest:.2f}"

# --- Using the class ---
account = BankAccount("Alice", 1000)

print(account.deposit(500))      # Deposited $500. New balance: $1500
print(account.withdraw(200))     # Withdrew $200. New balance: $1300
print(account.get_account_info())

# Private members are NOT accessible
# print(account.__balance)        # AttributeError!
# print(account.__transaction_history)  # AttributeError!
# print(account.__generate_account_number())  # AttributeError!

# But they can be accessed via name mangling (but don't do this!)
print(account._BankAccount__balance)  # 1300 — name mangled access
print(account._BankAccount__transaction_history)

# Subclass cannot access private members
savings = SavingsAccount("Bob", 1000, 2.5)
print(savings.apply_interest())   # Interest applied: $25.00
# print(savings.__balance)        # AttributeError!
# print(savings.__transaction_history)  # AttributeError!

print("\nāœ… Private members: double underscore → name mangling")
print("āŒ Cannot access directly from outside")
print("āš ļø Subclasses also cannot access private members of parent")
print("šŸ”§ Name mangling: _ClassName__name (but this is a hack)")

Private key points:

  • Double underscore — __name indicates private
  • Name mangling — changed to _ClassName__name
  • Not accessible outside — direct access raises AttributeError
  • Not accessible in subclasses — name mangling prevents
  • Can be accessed via mangled name — but this is a hack

Quick Check: How does Python make private members inaccessible from outside? (Answer: Name mangling — __name becomes _ClassName__name)

Name Mangling Explained

4

How Private Members Are Protected

Name mangling is Python's way of making private members "private." When you define an attribute with two leading underscores (__name), Python automatically changes the name to _ClassName__name.

This prevents accidental access from outside the class and prevents subclasses from accidentally overriding private methods.

# Name Mangling — How Private Members Work

class Parent:
    def __init__(self):
        self.__private = "I'm private"
        self._protected = "I'm protected"
        self.public = "I'm public"
    
    def __private_method(self):
        return "Private method called"
    
    def _protected_method(self):
        return "Protected method called"
    
    def public_method(self):
        return "Public method called"
    
    def show_private(self):
        # Can access private from inside the class
        return f"Inside: {self.__private}"

class Child(Parent):
    def __init__(self):
        super().__init__()
        # This creates a DIFFERENT attribute — not overriding parent's
        self.__private = "Child's private"
    
    def try_access_parent_private(self):
        # Cannot access parent's __private directly
        # return self.__private  # Returns child's __private, not parent's
        
        # Need to use the public method
        return self.show_private()
    
    def get_mangled_name(self):
        # The mangled name for parent's private
        return "Parent's __private becomes: _Parent__private"

# --- Demonstration ---
parent = Parent()
child = Child()

# Public — accessible
print(parent.public)              # I'm public
print(parent.public_method())     # Public method called

# Protected — accessible but discouraged
print(parent._protected)          # I'm protected
print(parent._protected_method()) # Protected method called

# Private — not directly accessible
# print(parent.__private)         # AttributeError!
# print(parent.__private_method()) # AttributeError!

# But we can see the mangled name
print(dir(parent))
# ... includes _Parent__private, _Parent__private_method

# Accessing via mangled name (hack — don't do this!)
print(parent._Parent__private)    # I'm private
print(parent._Parent__private_method())  # Private method called

# Child class
print(child._protected)           # I'm protected (inherited)
print(child.show_private())       # Inside: I'm private
print(child.try_access_parent_private())  # Inside: I'm private

# Child's own __private is different
print(child._Child__private)      # Child's private

print("\n" + "=" * 60)
print("NAME MANGLING SUMMARY")
print("=" * 60)
print("""
| Original Name | Mangled Name               | Purpose                    |
|---------------|----------------------------|----------------------------|
| __private     | _ClassName__private        | Prevent accidental access  |
| __method()    | _ClassName__method()       | Prevent subclass override  |

WHY NAME MANGLING?
1. Prevents accidental attribute access from outside
2. Prevents subclasses from accidentally overriding
3. Makes private truly private (to the class)
4. Follows the "consenting adults" principle

āš ļø Important: Name mangling is NOT security!
   It's a protection against accidents, not malicious access.
   Anyone can still access using the mangled name.
""")

Name mangling key points:

  • Transforms — __name → _ClassName__name
  • Prevents accidental access — from outside and subclasses
  • Allows overriding safely — subclasses can have their own __name
  • Not security — it's a convention, not a wall
  • Can be bypassed — using the mangled name directly

Quick Check: What is name mangling and why is it used? (Answer: Python changes __name to _ClassName__name to prevent accidental access and subclass overriding)

Comparison Table

5

Public vs Protected vs Private — Side by Side

# Access Modifiers — Complete Comparison

class AccessDemo:
    def __init__(self):
        self.public = "Public"          # No underscore
        self._protected = "Protected"   # Single underscore
        self.__private = "Private"      # Double underscore
    
    def public_method(self):
        return "Public method"
    
    def _protected_method(self):
        return "Protected method"
    
    def __private_method(self):
        return "Private method"
    
    def access_all(self):
        # Inside the class — all accessible
        return [
            self.public,
            self._protected,
            self.__private,
            self.public_method(),
            self._protected_method(),
            self.__private_method()
        ]

class Subclass(AccessDemo):
    def __init__(self):
        super().__init__()
        self.public = "Subclass public"
        self._protected = "Subclass protected"
        # self.__private = "Subclass private"  # This is different!
    
    def access_parent(self):
        # Can access public and protected from parent
        parent_public = super().public
        parent_protected = super()._protected
        
        # Cannot access parent's private directly
        # parent_private = super().__private  # AttributeError
        
        return parent_public, parent_protected

# ============================================================
# COMPARISON TABLE
# ============================================================

print("""
╔═══════════════════════╦═══════════════════════════════════════════════════════════════════════════════════╗
ā•‘ Access Modifier        ā•‘ Visibility                                                                       ā•‘
╠═══════════════════════╬═══════════════════════════════════════════════════════════════════════════════════╣
ā•‘                       ā•‘  • No underscore                                                                  ā•‘
ā•‘ PUBLIC                ā•‘  • Accessible from anywhere                                                       ā•‘
ā•‘ (name)                ā•‘  • Available in subclasses                                                         ā•‘
ā•‘                       ā•‘  • The default for all members                                                    ā•‘
╠═══════════════════════╬═══════════════════════════════════════════════════════════════════════════════════╣
ā•‘                       ā•‘  • Single underscore                                                              ā•‘
ā•‘ PROTECTED             ā•‘  • Accessible inside class and subclasses                                         ā•‘
ā•‘ (_name)               ā•‘  • Can be accessed from outside (discouraged)                                     ā•‘
ā•‘                       ā•‘  • Signals "internal use — don't touch"                                          ā•‘
╠═══════════════════════╬═══════════════════════════════════════════════════════════════════════════════════╣
ā•‘                       ā•‘  • Double underscore                                                              ā•‘
ā•‘ PRIVATE               ā•‘  • Name mangled: _ClassName__name                                                ā•‘
ā•‘ (__name)              ā•‘  • Not accessible from outside (directly)                                         ā•‘
ā•‘                       ā•‘  • Not accessible in subclasses                                                   ā•‘
ā•‘                       ā•‘  • Can be accessed via mangled name (hack)                                        ā•‘
ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•©ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•

USAGE RECOMMENDATIONS:
• PUBLIC:   Public API — safe for external use
• PROTECTED: Internal implementation — for subclasses
• PRIVATE:  Implementation detail — not for external use
""")

Comparison summary:

  • Public — no underscore, accessible everywhere
  • Protected — _name, for internal use and subclasses
  • Private — __name, name mangled, class-only
  • Outside access — public: yes, protected: yes (but discouraged), private: no (except hack)
  • Subclass access — public: yes, protected: yes, private: no

Quick Check: Which access modifier can subclasses access? (Answer: Public and protected — private is not accessible in subclasses)

Real-World Example

6

Building a Secure User System

# Real-World Example: User Management System

import hashlib
import secrets
from datetime import datetime, timedelta

class User:
    """Secure user management with access modifiers"""
    
    def __init__(self, username, email, password):
        self.username = username              # Public — user identifier
        self.email = email                    # Public — contact info
        self.__password_hash = None           # Private — password hash
        self.__failed_login_attempts = 0      # Private — security tracking
        self.__last_login = None              # Private — login tracking
        self.__session_token = None           # Private — authentication
        self.__is_locked = False              # Private — account lock
        self._role = "user"                   # Protected — for subclasses
    
        # Use protected method to set password
        self._set_password(password)
        self._log_event("Account created")
    
    # ---- PUBLIC API ----
    
    def login(self, password):
        """Public — authenticate user"""
        if self.__is_locked:
            return "Account is locked. Contact support."
        
        if self._verify_password(password):
            self.__failed_login_attempts = 0
            self.__last_login = datetime.now()
            self.__session_token = self._generate_token()
            self._log_event("Login successful")
            return f"Welcome back, {self.username}!"
        else:
            self.__failed_login_attempts += 1
            if self.__failed_login_attempts >= 3:
                self.__is_locked = True
                self._log_event("Account locked due to failed attempts")
                return "Account locked due to too many failed attempts."
            return "Invalid password."
    
    def change_password(self, old_password, new_password):
        """Public — change password with verification"""
        if not self._verify_password(old_password):
            self._log_event("Failed password change attempt")
            return "Invalid current password."
        
        if len(new_password) < 8:
            return "Password must be at least 8 characters."
        
        self._set_password(new_password)
        self.__session_token = None  # Invalidate old sessions
        self._log_event("Password changed")
        return "Password changed successfully."
    
    def logout(self):
        """Public — end session"""
        self.__session_token = None
        self._log_event("Logged out")
        return "Logged out successfully."
    
    def get_profile(self):
        """Public — get user info (safe)"""
        return {
            "username": self.username,
            "email": self.email,
            "role": self._role,
            "last_login": self.__last_login,
            "is_locked": self.__is_locked
        }
    
    def reset_account(self):
        """Public — reset security (admin only)"""
        self.__failed_login_attempts = 0
        self.__is_locked = False
        self.__session_token = None
        self._log_event("Account reset by admin")
        return "Account reset successfully."
    
    # ---- PROTECTED METHODS ----
    
    def _set_password(self, password):
        """Protected — hash and store password"""
        salt = secrets.token_hex(16)
        self.__password_hash = self._hash_password(password, salt)
    
    def _verify_password(self, password):
        """Protected — verify password against hash"""
        if self.__password_hash is None:
            return False
        # In real code, extract salt and verify
        # Simplified for demo
        return True
    
    def _hash_password(self, password, salt):
        """Protected — hash password with salt"""
        return hashlib.sha256((password + salt).encode()).hexdigest()
    
    def _generate_token(self):
        """Protected — generate session token"""
        return secrets.token_urlsafe(32)
    
    def _log_event(self, event):
        """Protected — log user activity"""
        print(f"[{datetime.now()}] {self.username}: {event}")
    
    # ---- PRIVATE METHODS (Implementation Details) ----
    
    def __validate_email(self):
        """Private — email validation (name mangled)"""
        # Implementation detail
        return "@" in self.email and "." in self.email

class AdminUser(User):
    """Admin user with additional privileges"""
    
    def __init__(self, username, email, password, admin_level):
        super().__init__(username, email, password)
        self._role = "admin"  # Protected — can modify
        self.__admin_level = admin_level  # Private
    
    def promote_user(self, user):
        """Admin action — promote a user"""
        if isinstance(user, User):
            user._role = "admin"  # Access protected member
            self._log_event(f"Promoted {user.username}")
            return f"{user.username} promoted to admin."
        return "Invalid user."
    
    def demote_user(self, user):
        """Admin action — demote a user"""
        if isinstance(user, User):
            user._role = "user"
            self._log_event(f"Demoted {user.username}")
            return f"{user.username} demoted to user."
        return "Invalid user."

# ---- DEMO ----

print("=" * 60)
print("USER MANAGEMENT SYSTEM")
print("=" * 60)

# Create users
print("\n1. CREATING USERS")
user1 = User("alice", "alice@example.com", "SecurePass123")
user2 = User("bob", "bob@example.com", "MyPassword456")
admin = AdminUser("admin", "admin@example.com", "AdminPass789", 5)

# Login attempts
print("\n2. LOGIN ATTEMPTS")
print(user1.login("wrong"))        # Invalid password.
print(user1.login("wrong"))        # Invalid password.
print(user1.login("wrong"))        # Account locked...
print(user1.login("SecurePass123")) # Account is locked. Contact support.

# Admin resets account
print("\n3. ADMIN ACTIONS")
print(admin.promote_user(user2))   # bob promoted to admin.
print(admin.reset_account(user1))  # Account reset successfully.

# User logs in again
print("\n4. LOGIN SUCCESS")
print(user1.login("SecurePass123")) # Welcome back, alice!

# Change password
print("\n5. CHANGE PASSWORD")
print(user1.change_password("SecurePass123", "NewStrongPass456"))

# Get profile
print("\n6. USER PROFILE")
print(user1.get_profile())

print("\nāœ… Public: username, email, profile methods")
print("āœ… Protected: _role, _set_password(), _log_event()")
print("āœ… Private: __password_hash, __failed_login_attempts, __session_token")
print("āœ… Encapsulation: sensitive data is protected")

Real-world example key points:

  • Public — username, email, login(), logout(), get_profile()
  • Protected — _role, _set_password(), _verify_password(), _log_event()
  • Private — __password_hash, __failed_login_attempts, __session_token, __is_locked
  • Encapsulation — sensitive data hidden behind public API
  • Security — password hashing, account locking, failed attempt tracking

Quick Check: Why are password hash and session token private? (Answer: To protect sensitive security data from accidental modification or access)

Best Practices

7

Using Access Modifiers Effectively

# Best Practices for Access Modifiers

# ============================================================
# 1. PUBLIC — The Safe API
# ============================================================

class SafeAPI:
    def __init__(self):
        self.name = "Public"  # Public — safe to access
        self._internal = "Internal"  # Protected — not part of API
        self.__secret = "Secret"  # Private — implementation detail
    
    def public_method(self):
        """This is the public API — documented and stable"""
        return "Use this method"
    
    def _internal_method(self):
        """Internal method — may change"""
        return "Don't depend on this"
    
    def __secret_method(self):
        """Implementation detail — don't touch"""
        return "This might change or disappear"

# āœ… DO: Keep public API clean and well-documented
# āœ… DO: Use public methods for all external interactions
# āŒ DON'T: Make internal details public


# ============================================================
# 2. PROTECTED — For Inheritance
# ============================================================

class BaseClass:
    def __init__(self):
        self._value = 0  # Protected — for subclasses
    
    def _helper_method(self):
        """Protected — subclasses can use this"""
        return self._value
    
    def public_method(self):
        return self._helper_method()  # Using protected method

class SubClass(BaseClass):
    def use_protected(self):
        # āœ… DO: Use protected members in subclasses
        return self._helper_method()
    
    def override_helper(self):
        # āœ… DO: Override protected methods
        self._value = 10
        return "Overridden"

# āœ… DO: Use protected for methods subclasses should override
# āœ… DO: Use protected for helpers that subclasses need
# āŒ DON'T: Expose protected members in the public API


# ============================================================
# 3. PRIVATE — For Implementation Details
# ============================================================

class DataProcessor:
    def __init__(self, data):
        self._data = data
        self.__cache = {}  # Private — caching implementation
        self.__processing_count = 0  # Private — state tracking
    
    def process(self):
        """Public API — stable and documented"""
        self.__processing_count += 1
        return self.__process_data()
    
    def __process_data(self):
        """Private — implementation detail"""
        if not self.__cache:
            self.__cache = self._build_cache()
        return self.__cache
    
    def _build_cache(self):
        """Protected — can be overridden by subclasses"""
        return {"processed": True}

# āœ… DO: Use private for implementation details
# āœ… DO: Use private for state that shouldn't change
# āœ… DO: Use private for methods that might be removed
# āŒ DON'T: Make everything private


# ============================================================
# 4. GETTERS AND SETTERS (Property Decorator)
# ============================================================

class Temperature:
    def __init__(self, celsius):
        self._celsius = celsius
        self._fahrenheit = celsius * 9/5 + 32
    
    @property
    def celsius(self):
        """Getter — public access"""
        return self._celsius
    
    @celsius.setter
    def celsius(self, value):
        """Setter — with validation"""
        if value < -273.15:
            raise ValueError("Temperature below absolute zero!")
        self._celsius = value
        self._fahrenheit = value * 9/5 + 32
    
    @property
    def fahrenheit(self):
        """Getter — computed property"""
        return self._fahrenheit
    
    @fahrenheit.setter
    def fahrenheit(self, value):
        self.celsius = (value - 32) * 5/9

# āœ… DO: Use properties for controlled access
# āœ… DO: Add validation in setters
# āœ… DO: Keep the internal representation private (_celsius)


# ============================================================
# 5. SUMMARY TABLE
# ============================================================

print("""
╔══════════════════════════════════════════════════════════════════════════════════╗
ā•‘ ACCESS MODIFIER BEST PRACTICES                                                  ā•‘
╠══════════════════════════════════════════════════════════════════════════════════╣
ā•‘                                                                                 ā•‘
ā•‘  PUBLIC (name)                                                                  ā•‘
ā•‘  • Use for the public API of your class                                         ā•‘
ā•‘  • Document these well                                                          ā•‘
ā•‘  • Keep them stable — avoid breaking changes                                    ā•‘
ā•‘  • This is what users of your class interact with                               ā•‘
ā•‘                                                                                 ā•‘
ā•‘  PROTECTED (_name)                                                              ā•‘
ā•‘  • Use for internal helpers that subclasses might need                          ā•‘
ā•‘  • Use for methods that subclasses should override                              ā•‘
ā•‘  • Use for attributes that subclasses need access to                            ā•‘
ā•‘  • Document that these are for internal use                                     ā•‘
ā•‘                                                                                 ā•‘
ā•‘  PRIVATE (__name)                                                               ā•‘
ā•‘  • Use for implementation details that should be hidden                         ā•‘
ā•‘  • Use for sensitive data (passwords, tokens, etc.)                             ā•‘
ā•‘  • Use for internal state that should not be accessed                           ā•‘
ā•‘  • Use for methods that might be removed or changed                             ā•‘
ā•‘                                                                                 ā•‘
ā•‘  REMEMBER:                                                                      ā•‘
ā•‘  • Protected and private are CONVENTIONS, not security                          ā•‘
ā•‘  • Python trusts developers to respect the conventions                          ā•‘
ā•‘  • Private uses name mangling to prevent accidental access                      ā•‘
ā•‘                                                                                 ā•‘
╠══════════════════════════════════════════════════════════════════════════════════╣
ā•‘  "We're all consenting adults here." — Python Philosophy                       ā•‘
ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•
""")

Best practices summary:

  • Public — the stable API, document well, avoid breaking changes
  • Protected — for subclasses, internal helpers, override points
  • Private — implementation details, sensitive data, can change
  • Properties — use @property for controlled access
  • Remember — these are conventions, not security walls

Quick Check: What should you use protected members for? (Answer: Internal helpers that subclasses might need or override)

Try It Yourself

Experiment with access modifiers in the editor below.

Loading Pyodide... 0%
Python Code Editor
==================================================
ACCESS MODIFIERS - PRACTICE
==================================================

1. CLASS WITH PUBLIC, PROTECTED, PRIVATE

2. USING THE BANK ACCOUNT
āœ… Deposited $500. Balance: $1500
āœ… Withdrew $200. Balance: $1300
Balance: $1300
Transactions: ['Deposit: +$500', 'Withdrawal: -$200']

3. ACCESSING PROTECTED AND PRIVATE
Protected _type: savings (accessible but discouraged)
āŒ Cannot access __balance: 'BankAccount' object has no attribute '__balance'
Name mangling: 1300 (hack)

4. SUBCLASS (SAVINGS ACCOUNT)
šŸ’° Interest applied: $50.00
Balance: $2050.0

==================================================
SUMMARY:
āœ… Public: owner, deposit(), withdraw(), get_balance()
āœ… Protected: _type, _add_transaction()
āœ… Private: __balance, __transactions, __calculate_interest()
āœ… Name mangling: _BankAccount__balance
==================================================
šŸ†

You've Got It!

You now understand public, private, and protected access modifiers in Python. You know how to use underscores for visibility control and how name mangling works.

Quick Quiz

Test what you've learned:

1. What is the default access modifier in Python?
2. How do you indicate a protected member in Python?
3. What is name mangling?
4. Can a subclass access a private member of its parent class?
5. What is the purpose of access modifiers in Python?

Frequently Asked Questions

Does Python have public, private, and protected like Java? ā–¼

Python has similar concepts but uses conventions rather than keywords. Public is the default, protected uses a single underscore (_name), and private uses a double underscore (__name). However, these are not enforced — they're signals to other developers.

Can I access a private member from outside the class? ā–¼

Yes, but not directly. Private members are name-mangled to _ClassName__name. You can access them using the mangled name, but you shouldn't. This is considered a hack and violates the encapsulation principle.

What's the difference between protected and private? ā–¼

Protected (_name) is a convention for "internal use, but subclasses can access it." Private (__name) uses name mangling and is harder to access from outside the class. Private members are not accessible in subclasses.

When should I use protected vs private? ā–¼

Use protected when you expect subclasses to need access or override the method. Use private for implementation details that shouldn't be accessed by anyone outside the class (including subclasses). Private is for "implementation details that can change."

Why doesn't Python enforce access modifiers? ā–¼

Python follows the philosophy of "We're all consenting adults." The language trusts developers to use the conventions properly. Enforcing access control would make Python less flexible and go against its design principles.

What is the purpose of name mangling? ā–¼

Name mangling prevents accidental access and accidental overriding. It's not for security — it's to prevent bugs where subclasses accidentally override private methods or access private attributes they shouldn't.

Where to Go From Here

Now that you understand access modifiers in Python, check out these related topics:

Encapsulation in Python

Learn more about encapsulating data and implementation details.

Learn More →

Inheritance in Python

Learn how protected members work with inheritance hierarchies.

Learn More →

Property Decorator

Learn how to use @property for controlled attribute access.

Learn More →
Interview Resources
  • Python Syntax & Variables Interview Questions
  • Top SQL Interview Questions & Answers
  • SQL Joins: Displaying Data from Multiple Tables FAQ
  • Python Lists and Dictionaries Interview Questions
  • Python OOP Interview Questions
  • SQL Set Operators Interview Questions
Previous: Abstract Class vs Interface Next: Overloading vs Overriding →