🔐 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.
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?
Track Your Learning Progress
Sign in to bookmark tutorials and keep track of your learning journey.
Your progress is saved automatically as you read.