Generally in Object-Oriented Programming(OOP), attributes/properties and methods have 3 levels of access-
- Public – anyone can access it from anywhere
- Protected – only the same class and subclass can access
- Private – only the same class can access
And we generally use the keyword “public“, “protected“, and “private“, to set the access control of an attribute/property or method.
WARNING
Python does access control differently.
Let’s discuss access control in Python in depth-
Public
Just normally declaring attributes and methods will make it public. Here are the criteria/behavior of public attributes and methods in Python-
class Product:
def __init__(self, name: str) -> None:
self.name = name # Public attribute
# Public method
def get_info(self) -> str:
return f"Name: {self.name}"
class Electronics(Product):
def __init__(self, name: str, brand: str) -> None:
super().__init__(name)
# Additional attributes for Electronics
self.brand = brand # Public attribute
# Additional public method to display electronics details
def get_info(self) -> str:
return f"{super().get_info()}, Brand: {self.brand}"
# Example usage
product = Product("Head first design pattern")
# Access public attribute and method
print(f"Product name: {product.name}")
print(f"Product full info:\n{product.get_info()}")
# Electronics example usage
electronics = Electronics("Sonic Wireless Headphone", "Sonic Electronics")
# Access public attribute and method
print(f"Electronics name: {electronics.name}")
print(f"Electronics brand: {electronics.brand}")
print(f"Product electronics info:\n{electronics.get_info()}")
PythonOutput:
Product name: Head first design pattern
Product full info:
Name: Head first design pattern
Electronics name: Sonic Wireless Headphone
Electronics brand: Sonic Electronics
Product electronics info:
Name: Sonic Wireless Headphone, Brand: Sonic Electronics
PlaintextProtected
WARNING
Python does not have any way for the “protected” access control.
This can be done by following convention(prefix with a single underscore), but it will not put any restrictions on the attribute or method.
Add a single underscore(_) before any attribute or method, and it will be considered as protected(by convention).
As protected access can not be enforced, so by the convention we expect the following behavior from a protected attribute and method-
class Product:
def __init__(self, name: str, price: float) -> None:
self.name = name # Public attribute
self._price = price # Protected attribute (single underscore convention)
# Public method
def get_info(self) -> str:
return f"Name: {self.name}, Price: {self._price}"
# Protected method
def _apply_discount(self, discount: float) -> None:
if 0 > discount > 100:
raise ValueError("Discount value is out of range")
self._price = self._price * (1 - discount / 100)
class Electronics(Product):
def __init__(self, name: str, price: float, brand: str) -> None:
super().__init__(name, price)
# Additional attributes for Electronics
self.brand = brand # Public attribute
# Additional public method to display electronics details
def get_info(self) -> str:
return f"{super().get_info()}, Brand: {self.brand}"
# Example usage
product = Product("Head first design pattern", 100)
# Access public attribute and method
print(f"Product name: {product.name}")
print(f"Product full info:\n{product.get_info()}")
# By convention the _price attribute and _apply_discount method is protected
# We are not suposed to access it directly(by convention)
# Though these can be accessed directly, and the interpreter will not complain
print(f"Price before discount: {product._price}")
product._apply_discount(15)
print(f"Price after discount: {product._price}")
# Electronics example usage
electronics = Electronics("Sonic Wireless Headphone", 29.90, "Sonic Electronics")
# Access public attribute and method
print(f"Electronics name: {electronics.name}")
print(f"Electronics brand: {electronics.brand}")
print(f"Product electronics info:\n{electronics.get_info()}")
PythonOutput:
Product name: Head first design pattern
Product full info:
Name: Head first design pattern, Price: 100
Price before discount: 100
Price after discount: 85.0
Electronics name: Sonic Wireless Headphone
Electronics brand: Sonic Electronics
Product electronics info:
Name: Sonic Wireless Headphone, Price: 29.9, Brand: Sonic Electronics
PlaintextPrivate
To make an attribute or method private, prefix the attribute or method with 2 underscores(__). Private attributes and methods have the following behavior-
WARNING
However, the private attributes and methods can be accessed by using the format _ClassName__attributeName and _ClassName__methodName, because of name mangling in Python(internally). But it is not recommended to access private properties, in that way.
We have discussed Python name mangling later in this tutorial.
Check the example below-
class Product:
def __init__(self, name: str, price: float, stock: int) -> None:
self.name = name # Public attribute
self._price = price # Protected attribute (single underscore convention)
self.__stock = stock # Private attribute (double underscore convention)
# Public method
def get_info(self) -> str:
return f"Name: {self.name}, Price: {self._price}"
# Protected method
def _apply_discount(self, discount: float) -> None:
if 0 > discount > 100:
raise ValueError("Discount value is out of range")
self._price = self._price * (1 - discount / 100)
# Private method
def __get_stock(self):
return self.__stock
# Public method for stock update
def update_stock(self, amount) -> None:
if amount < 0:
raise ValueError("Stock can not be negative")
self.__stock = amount
class Electronics(Product):
def __init__(self, name: str, price: float, stock: int, brand: str) -> None:
super().__init__(name, price, stock)
# Additional attributes for Electronics
self.brand = brand # Public attribute
# Additional public method to display electronics details
def get_info(self) -> str:
return f"{super().get_info()}, Brand: {self.brand}"
# Try to use a private method in the subclass
def check_stock(self) -> None:
return self.__get_stock()
# Example usage
product = Product("Head first design pattern", 100, 99)
# Access public attribute and method
print(f"Product name: {product.name}")
print(f"Product full info:\n{product.get_info()}")
# Accessing private attribute and method (will raise AttributeError)
try:
print(product.__stock) # will raise an error
except Exception as err:
print(err)
try:
print(product.__get_stock()) # will raise an error
except Exception as err:
print(err)
# Electronics example usage
electronics = Electronics(
name="Sonic Wireless Headphone", price=29.90, brand="Sonic Electronics", stock=10
)
# Access public attribute and method
print(f"Electronics name: {electronics.name}")
print(f"Electronics brand: {electronics.brand}")
print(f"Product electronics info:\n{electronics.get_info()}")
try:
print(electronics.check_stock())
except Exception as err:
print(err)
PythonOutput:
Product name: Head first design pattern
Product full info:
Name: Head first design pattern, Price: 100
'Product' object has no attribute '__stock'
'Product' object has no attribute '__get_stock'
Electronics name: Sonic Wireless Headphone
Electronics brand: Sonic Electronics
Product electronics info:
Name: Sonic Wireless Headphone, Price: 29.9, Brand: Sonic Electronics
'Electronics' object has no attribute '_Electronics__get_stock'
PlaintextName Mangling
Name mangling in Python, is a process where Python internally changes the name of the private attribute and method name to some specific format. So that it becomes harder to access or change those properties accidentally.
This is done just to make it a little harder to access the attribute or method.
If you check the private attributes and methods name of an object, you will see the class name with an underscore is added as the prefix. For example, in the Product class, the attribute name __stock is changed to _Product__stock.
WARNING
The private property or method can be accessed with this new/changed name.
But it is discouraged to access the mangled names of attributes or methods, unless absolutely necessary.
Check the example below-
class Product:
def __init__(self, name: str, price: float, stock: int) -> None:
self.name = name # Public attribute
self._price = price # Protected attribute (single underscore convention)
self.__stock = stock # Private attribute (double underscore convention)
# Public method
def get_info(self) -> str:
return f"Name: {self.name}, Price: {self._price}"
# Protected method
def _apply_discount(self, discount: float) -> None:
if 0 > discount > 100:
raise ValueError("Discount value is out of range")
self._price = self._price * (1 - discount / 100)
# Private method
def __get_stock(self):
return self.__stock
# Usage example
product = Product("Sonic wireless headphone", 100, 50)
try:
print(product.__stock) # will raise an error
except Exception as err:
print(err)
try:
print(product.__get_stock()) # will raise an error
except Exception as err:
print(err)
print(dir(product))
print(product._Product__stock) # Private attribute can still be accessed with new name
print(product._Product__get_stock()) # Private method can be called with changed name
PythonOutput:
You can see the names of private attributes and methods are changed-
'Product' object has no attribute '__stock'
'Product' object has no attribute '__get_stock'
['_Product__get_stock', '_Product__stock', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_apply_discount', '_price', 'get_info', 'name']
50
50
PlaintextWe might need to use the mangled name of the Python attribute and method in the following cases-