🔐 Variable Visibility

Variable visibility controls how attributes and methods can be accessed from outside a class. Python uses naming conventions to indicate different visibility levels, helping you create well-encapsulated objects that protect their internal data while providing appropriate external interfaces.

Think of variable visibility like rooms in your house - some areas are public (living room for guests), some are protected (family room for family members), and others are private (your personal bedroom). Similarly, class attributes can have different access levels based on their intended use.

# Variable visibility in action
class BankAccount:
    def __init__(self, owner, initial_balance):
        self.owner = owner              # Public attribute
        self._account_type = "checking" # Protected attribute
        self.__balance = initial_balance # Private attribute
    
    def deposit(self, amount):
        if amount > 0:
            self.__balance += amount
            return f"Deposited ${amount}. Balance: ${self.get_balance()}"
        return "Invalid deposit amount"
    
    def get_balance(self):
        return self.__balance  # Controlled access to private data
    
    def _validate_transaction(self, amount):  # Protected method
        return amount > 0 and amount <= self.__balance

# Different visibility levels
account = BankAccount("Alice", 1000)
print(account.owner)           # Public - accessible
print(account._account_type)   # Protected - accessible but not recommended
# print(account.__balance)     # Private - not directly accessible
print(account.get_balance())   # Access private data through public method

🎯 Understanding Visibility Levels

Python uses naming conventions to indicate different levels of variable and method visibility, creating implicit access control that guides how class members should be used.

Public Attributes

Public attributes form the main interface of your class, providing data and functionality that external code is expected to use directly.

class Book:
    def __init__(self, title, author, isbn):
        # Public attributes - part of the class interface
        self.title = title
        self.author = author
        self.isbn = isbn
        self.is_available = True
        self.checkout_count = 0
    
    def checkout(self):
        if self.is_available:
            self.is_available = False
            self.checkout_count += 1
            return f"Checked out: {self.title}"
        return "Book already checked out"
    
    def return_book(self):
        self.is_available = True
        return f"Returned: {self.title}"

# Public attributes can be accessed and modified directly
book = Book("Python Guide", "John Doe", "123-456-789")
print(f"Title: {book.title}")           # Direct access
print(f"Available: {book.is_available}") # Direct access

book.checkout()
print(f"Checkout count: {book.checkout_count}")  # Direct access
book.title = "Advanced Python Guide"    # Direct modification allowed
print(f"New title: {book.title}")

Protected Attributes

Protected attributes are intended for internal use and subclasses. The single underscore prefix signals that these attributes are implementation details that may change.

class Vehicle:
    def __init__(self, make, model):
        self.make = make              # Public
        self.model = model            # Public
        self._engine_status = "off"   # Protected
        self._maintenance_due = False # Protected
    
    def start(self):
        if self._check_engine_ready():  # Using protected method
            self._engine_status = "on"
            return f"{self.make} {self.model} started"
        return "Engine not ready"
    
    def _check_engine_ready(self):  # Protected method
        return not self._maintenance_due
    
    def _schedule_maintenance(self):  # Protected method
        self._maintenance_due = True

class Car(Vehicle):
    def __init__(self, make, model, doors):
        super().__init__(make, model)
        self.doors = doors
        self._fuel_level = 100        # Protected in subclass
    
    def drive(self):
        if self._engine_status == "on" and self._fuel_level > 0:
            self._fuel_level -= 10
            return f"Driving {self.make} {self.model}"
        return "Cannot drive - check engine and fuel"
    
    def get_status(self):
        # Subclass can access parent's protected attributes
        return f"Engine: {self._engine_status}, Fuel: {self._fuel_level}%"

# Protected attributes accessible but not recommended for external use
car = Car("Toyota", "Camry", 4)
print(car.start())
print(car.get_status())

# You CAN access protected attributes, but you shouldn't
print(f"Engine status: {car._engine_status}")  # Works but discouraged

Private Attributes

Private attributes use name mangling to make external access difficult, truly protecting internal implementation details from outside interference.

class SecureBankAccount:
    def __init__(self, owner, pin, initial_balance):
        self.owner = owner                    # Public
        self._account_number = "ACC123456"    # Protected
        self.__pin = pin                      # Private
        self.__balance = initial_balance      # Private
        self.__transaction_log = []           # Private
    
    def authenticate(self, entered_pin):
        return self.__pin == entered_pin
    
    def withdraw(self, amount, pin):
        if not self.authenticate(pin):
            self.__log_transaction("Failed authentication")
            return "Authentication failed"
        
        if amount > self.__balance:
            self.__log_transaction(f"Failed withdrawal: ${amount}")
            return "Insufficient funds"
        
        self.__balance -= amount
        self.__log_transaction(f"Withdrawal: ${amount}")
        return f"Withdrew ${amount}. Balance: ${self.__balance}"
    
    def get_balance(self, pin):
        if self.authenticate(pin):
            return self.__balance
        return "Authentication required"
    
    def __log_transaction(self, transaction):  # Private method
        import datetime
        self.__transaction_log.append({
            "time": datetime.datetime.now(),
            "action": transaction
        })
    
    def get_recent_transactions(self, pin, count=5):
        if self.authenticate(pin):
            return self.__transaction_log[-count:]
        return "Authentication required"

# Private attributes are truly protected
account = SecureBankAccount("Alice", "1234", 1000)
print(account.withdraw(100, "1234"))
print(account.get_balance("1234"))

# These won't work as expected
# print(account.__pin)      # AttributeError
# print(account.__balance)  # AttributeError

# Name mangling makes private attributes accessible but difficult
print(f"Mangled name access: {account._SecureBankAccount__balance}")  # Works but strongly discouraged

⚡ Name Mangling Mechanism

Python implements privacy through name mangling, which automatically renames private attributes to make them harder to access accidentally.

How Name Mangling Works

When Python encounters a private attribute (starting with double underscore), it automatically renames it by adding the class name prefix.

class Example:
    def __init__(self):
        self.public = "I'm public"
        self._protected = "I'm protected"
        self.__private = "I'm private"
    
    def show_attributes(self):
        print(f"Public: {self.public}")
        print(f"Protected: {self._protected}")
        print(f"Private: {self.__private}")  # Works inside class

# Name mangling demonstration
obj = Example()
obj.show_attributes()

# Check how attributes are actually stored
print("\nActual attribute names:")
for attr in dir(obj):
    if not attr.startswith('__') or attr.endswith('__'):
        continue
    print(f"  {attr}")

# Accessing mangled name (not recommended)
print(f"\nMangled access: {obj._Example__private}")

When Name Mangling Occurs

Name mangling only applies to attributes that start with double underscores but don't end with them.

class ManglingDemo:
    def __init__(self):
        self.normal = "Normal attribute"
        self._protected = "Protected attribute"
        self.__private = "Private attribute (mangled)"
        self.__special__ = "Special method (not mangled)"
    
    def __str__(self):  # Special method (not mangled)
        return "ManglingDemo instance"
    
    def __private_method(self):  # Private method (mangled)
        return "This is a private method"
    
    def _protected_method(self):  # Protected method (not mangled)
        return "This is a protected method"
    
    def public_method(self):  # Public method (not mangled)
        return "This is a public method"

demo = ManglingDemo()
print("Attributes without double underscore prefix/suffix:")
for attr in sorted(dir(demo)):
    if not attr.startswith('_'):
        print(f"  {attr}")

print("\nMangled attributes (private):")
for attr in sorted(dir(demo)):
    if attr.startswith('_ManglingDemo__'):
        print(f"  {attr}")

🚀 Access Control Patterns

Property-Based Access Control

Use properties to create controlled access to attributes while maintaining a clean interface.

class Temperature:
    def __init__(self, celsius=0):
        self.__celsius = celsius  # Private storage
    
    @property
    def celsius(self):
        """Get temperature in Celsius"""
        return self.__celsius
    
    @celsius.setter
    def celsius(self, value):
        """Set temperature in Celsius with validation"""
        if value < -273.15:
            raise ValueError("Temperature cannot be below absolute zero")
        self.__celsius = value
    
    @property
    def fahrenheit(self):
        """Get temperature in Fahrenheit"""
        return (self.__celsius * 9/5) + 32
    
    @fahrenheit.setter
    def fahrenheit(self, value):
        """Set temperature in Fahrenheit"""
        celsius = (value - 32) * 5/9
        self.celsius = celsius  # Use the celsius setter for validation
    
    @property
    def kelvin(self):
        """Get temperature in Kelvin"""
        return self.__celsius + 273.15

# Property-based access control
temp = Temperature(25)
print(f"Celsius: {temp.celsius}")
print(f"Fahrenheit: {temp.fahrenheit}")
print(f"Kelvin: {temp.kelvin}")

# Controlled modification
temp.fahrenheit = 100  # Sets internal celsius value
print(f"After setting to 100°F: {temp.celsius:.2f}°C")

# Validation in action
try:
    temp.celsius = -300  # This will raise an error
except ValueError as e:
    print(f"Error: {e}")

Encapsulation with Method Control

Create controlled access to complex operations through carefully designed public methods.

class BankVault:
    def __init__(self, initial_contents=None):
        self.__contents = initial_contents or {}
        self.__access_log = []
        self.__is_locked = True
        self.__master_key = "secret_key_123"
    
    def unlock(self, key):
        """Unlock the vault with the master key"""
        if key == self.__master_key:
            self.__is_locked = False
            self.__log_access("Vault unlocked")
            return "Vault unlocked successfully"
        else:
            self.__log_access("Failed unlock attempt")
            return "Invalid key"
    
    def lock(self):
        """Lock the vault"""
        self.__is_locked = True
        self.__log_access("Vault locked")
        return "Vault locked"
    
    def store_item(self, item_id, item):
        """Store an item in the vault"""
        if self.__is_locked:
            return "Vault is locked. Please unlock first."
        
        self.__contents[item_id] = item
        self.__log_access(f"Stored item: {item_id}")
        return f"Item {item_id} stored successfully"
    
    def retrieve_item(self, item_id):
        """Retrieve an item from the vault"""
        if self.__is_locked:
            return "Vault is locked. Please unlock first."
        
        if item_id in self.__contents:
            item = self.__contents.pop(item_id)
            self.__log_access(f"Retrieved item: {item_id}")
            return item
        else:
            self.__log_access(f"Failed retrieval: {item_id}")
            return "Item not found"
    
    def get_inventory(self):
        """Get list of items without revealing contents"""
        if self.__is_locked:
            return "Vault is locked"
        return list(self.__contents.keys())
    
    def __log_access(self, action):
        """Private method to log access attempts"""
        import datetime
        self.__access_log.append({
            "time": datetime.datetime.now(),
            "action": action
        })
    
    def get_access_log(self, key):
        """Get access log with master key"""
        if key == self.__master_key:
            return self.__access_log[-5:]  # Last 5 entries
        return "Access denied"

# Controlled vault access
vault = BankVault()
print(vault.store_item("gold_bar", "1kg Gold Bar"))  # Fails - locked
print(vault.unlock("wrong_key"))  # Fails - wrong key
print(vault.unlock("secret_key_123"))  # Succeeds
print(vault.store_item("gold_bar", "1kg Gold Bar"))  # Succeeds
print(vault.get_inventory())  # Shows item IDs
print(vault.lock())  # Lock vault

🌟 Best Practices and Design Patterns

Visibility Guidelines

Choose the appropriate visibility level based on the intended use and future maintainability.

Common Patterns

Apply these established patterns for effective encapsulation.

class ConfigManager:
    def __init__(self, config_file):
        self.__config_file = config_file        # Private - file path
        self.__config_data = {}                 # Private - loaded data
        self._cache_timeout = 300               # Protected - cache setting
        self.auto_save = True                   # Public - user setting
        self.__load_config()
    
    # Public interface
    def get_setting(self, key, default=None):
        """Public method to get configuration values"""
        return self.__config_data.get(key, default)
    
    def set_setting(self, key, value):
        """Public method to set configuration values"""
        self.__config_data[key] = value
        if self.auto_save:
            self.__save_config()
        return f"Setting {key} updated"
    
    def save(self):
        """Public method to manually save configuration"""
        return self.__save_config()
    
    # Protected interface (for subclasses)
    def _validate_setting(self, key, value):
        """Protected method for validation logic"""
        # Subclasses can override this
        return True
    
    def _get_cache_timeout(self):
        """Protected method to get cache timeout"""
        return self._cache_timeout
    
    # Private implementation
    def __load_config(self):
        """Private method to load configuration from file"""
        # Simulate loading from file
        self.__config_data = {
            "theme": "dark",
            "language": "en",
            "debug": False
        }
    
    def __save_config(self):
        """Private method to save configuration to file"""
        # Simulate saving to file
        return f"Configuration saved to {self.__config_file}"
    
    def __str__(self):
        """Public interface through special method"""
        return f"ConfigManager({len(self.__config_data)} settings)"

# Pattern usage
config = ConfigManager("app.config")
print(config.get_setting("theme"))
print(config.set_setting("theme", "light"))
print(config)

# Protected access (discouraged but possible)
print(f"Cache timeout: {config._get_cache_timeout()}")

# Private access (not recommended)
# print(config.__config_data)  # AttributeError

Hands-on Exercise

Create a simple BankAccount class that demonstrates variable visibility. Use public attributes for account holder name, protected attributes for account type, and private attributes for balance and PIN. Add methods to access and modify these attributes appropriately.

python
class BankAccount:
    def __init__(self, account_holder, account_type, initial_balance, pin):
        # TODO: Set public attribute for account holder name
        # TODO: Set protected attribute for account type
        # TODO: Set private attributes for balance and PIN
        pass
    
    def deposit(self, amount):
        # TODO: Add money to balance
        pass
    
    def withdraw(self, amount, pin):
        # TODO: Remove money if PIN is correct and balance sufficient
        pass
    
    def get_balance(self, pin):
        # TODO: Return balance if PIN is correct
        pass
    
    def change_pin(self, old_pin, new_pin):
        # TODO: Change PIN if old PIN is correct
        pass

# TODO: Test your bank account
account = BankAccount("Alice", "savings", 1000, 1234)
print(f"Account holder: {account.account_holder_name}")  # Public access
print(f"Account type: {account._account_type}")  # Protected access (discouraged but possible)
print(account.get_balance(1234))  # Access private balance through method
account.deposit(500)
print(account.withdraw(200, 1234))

Solution and Explanation 💡

Click to see the complete solution
class BankAccount:
    def __init__(self, account_holder, account_type, initial_balance, pin):
        # Public attribute for account holder name
        self.account_holder_name = account_holder
        # Protected attribute for account type
        self._account_type = account_type
        # Private attributes for balance and PIN
        self.__balance = initial_balance
        self.__pin = pin
    
    def deposit(self, amount):
        # Add money to balance
        if amount > 0:
            self.__balance += amount
            return f"Deposited ${amount}. New balance: ${self.__balance}"
        return "Invalid deposit amount"
    
    def withdraw(self, amount, pin):
        # Remove money if PIN is correct and balance sufficient
        if pin != self.__pin:
            return "Invalid PIN"
        if amount <= 0:
            return "Invalid withdrawal amount"
        if amount > self.__balance:
            return "Insufficient funds"
        
        self.__balance -= amount
        return f"Withdrew ${amount}. New balance: ${self.__balance}"
    
    def get_balance(self, pin):
        # Return balance if PIN is correct
        if pin == self.__pin:
            return f"Current balance: ${self.__balance}"
        return "Invalid PIN"
    
    def change_pin(self, old_pin, new_pin):
        # Change PIN if old PIN is correct
        if old_pin == self.__pin:
            self.__pin = new_pin
            return "PIN changed successfully"
        return "Invalid current PIN"

# Test your bank account
account = BankAccount("Alice", "savings", 1000, 1234)
print(f"Account holder: {account.account_holder_name}")  # Public access
print(f"Account type: {account._account_type}")  # Protected access (discouraged but possible)
print(account.get_balance(1234))  # Access private balance through method
account.deposit(500)
print(account.withdraw(200, 1234))

Key Learning Points:

  • 📌 Public attributes: No leading underscores, freely accessible from outside
  • 📌 Protected attributes: Single underscore _attribute, internal use (discouraged access)
  • 📌 Private attributes: Double underscore __attribute, name mangling prevents direct access
  • 📌 Controlled access: Use methods to safely access and modify private data
  • 📌 Encapsulation: Hide sensitive data while providing safe public interface

Learn more about object methods to understand how methods work with different visibility levels.

Test Your Knowledge

Test what you've learned about variable visibility:

What's Next?

Congratulations! You've completed the core object programming concepts. You now understand classes, objects, methods, inheritance, overriding, and visibility. These fundamentals prepare you for advanced topics like special methods, decorators, and design patterns.

Ready to explore more advanced concepts? Check out our lesson on Special Methods.

Was this helpful?

😔Poor
🙁Fair
😊Good
😄Great
🤩Excellent