Python: Access Control(attribute and method)

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.

Public: by default, everything is public in Python.
Private: add two(2) underscores(__) before the name of attributes or methods to make it private.
Protected: we can not make anything protected in Python. (Though there is a workaround)

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-

Everything by default is public.
Public attributes and methods can be accessed from anywhere(from inside and outside the class).
Can be accessed from subclasses using the dot(.) notation on “self” as if the attribute and method belong to the class.
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()}")
Python

Output:

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
Plaintext

Protected

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-

Should be accessed only from the same class, and subclasses.
Should not be overridden by subclasses.
Should not be accessed from outside, by convention.
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()}")
Python

Output:

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
Plaintext

Private

To make an attribute or method private, prefix the attribute or method with 2 underscores(__). Private attributes and methods have the following behavior-

Not directly accessible from outside.
Not accessible from subclasses.
Can only be accessed within the same class.

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)
Python

Output:

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'
Plaintext

Name 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
Python

Output:

You can see the names of private attributes and methods are changed-

__stock property name changed to _Product__stock
__get_stock name changed to _Product__get_stock
'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
Plaintext

We might need to use the mangled name of the Python attribute and method in the following cases-

Debugging: in cases when we want to debug the private attribute or method but we don’t want to make changes in the original attribute or method.
In Subclass: in some cases where we want to access a private property but we don’t want to change the original property name in the parent class, for some reason.

Leave a Comment


The reCAPTCHA verification period has expired. Please reload the page.