Part 10

Access modifiers

If a trait is defined as private in the base class, it is not directly accessible in any derived classes, as was briefly mentioned in the previous section. Let's take a look at an example. In the Notebook class below the notes are stored in a list, and the list attribute is private:


class Notebook:
    """ A Notebook stores notes in string format """

    def __init__(self):
        # private attribute
        self.__notes = []

    def add_note(self, note):
        self.__notes.append(note)

    def retrieve_note(self, index):
        return self.__notes[index]

    def all_notes(self):
        return ",".join(self.__notes)

If the integrity of the class is key, making the list attribute notes private makes sense. The class provides the client with suitable methods for adding and browsing notes, after all. This approach becomes problematic if we define a new class NotebookPro, which inherits the Notebook class. The private list attribute is not accessible to the client, but neither is it accessible to the derived classes. If we try to access it, as in the find_notes method below, we get an error:

class NotebookPro(Notebook):
    """ A better Notebook with search functionality """
    def __init__(self):
        # This is OK, the constructor is public despite the underscores
        super().__init__()

    # This causes an error
    def find_notes(self, search_term):
        found = []
        # the attribute __notes is private
        # the derived class can't access it directly
        for note in self.__notes:
            if search_term in note:
                found.append(note)

        return found
Sample output

AttributeError: 'NotebookPro' object has no attribute '_NotebookPro__notes'

Protected traits

Many object oriented programming languages have a feature, usually a special keyword, for protecting traits. This means that a trait should be hidden from the clients of the class, but kept accessible to its subclasses. Python in general abhors keywords, so no such feature is directly available in Python. Instead, there is a convention of marking protected traits in a certain way.

Remember, a trait can be hidden by prefixing its name with two underscores:

def __init__(self):
    self.__notes = []

The agreed convention to protect a trait is to prefix the name with a single underscore. Now, this is just a convention. Nothing prevents a programmer from breaking the convention, but it is considered a bad programming practice.

def __init__(self):
    self._notes = []

Below we have the entire Notebook example, with protected _notes instead of private __notes:

class Notebook:
    """ A Notebook stores notes in string format """

    def __init__(self):
        # protected attribute
        self._notes = []

    def add_note(self, note):
        self._notes.append(note)

    def retrieve_note(self, index):
        return self._notes[index]

    def all_notes(self):
        return ",".join(self._notes)

class NotebookPro(Notebook):
    """ A better Notebook with search functionality """
    def __init__(self):
        # This is OK, the constructor is public despite the underscores
        super().__init__()

    # This works, the protected attribute is accessible to the derived class
    def find_notes(self, search_term):
        found = []
        for note in self._notes:
            if search_term in note:
                found.append(note)

        return found

Below we have a handy table for the visibility of attributes with different access modifiers:

Access modifierExampleVisible to clientVisible to derived class
Publicself.nameyesyes
Protectedself._namenoyes
Privateself.__namenono

Access modifiers work the same with all traits. For example, in the Person class below we have the protected method capitalize_initials. It can be used from the derived class Footballer:

class Person:
    def __init__(self, name: str):
        self._name = self._capitalize_initials(name)

    def _capitalize_initials(self, name):
        name_capitalized = []
        for n in name.split(" "):
            name_capitalized.append(n.capitalize())

        return " ".join(name_capitalized)

    def __repr__(self):
        return self.__name

class Footballer(Person):

    def __init__(self, name: str, nickname: str, position: str):
        super().__init__(name)
        # the method is available as it is protected in the base class
        self.__nickname = self._capitalize_initials(nickname)
        self.__position = position

    def __repr__(self):
        r =  f"Footballer - name: {self._name}, nickname: {self.__nickname}"
        r += f", position: {self.__position}"
        return r

# Test the classes
if __name__ == "__main__":
    jp = Footballer("peter pythons", "pyper", "forward")
    print(jp)
Sample output

Footballer - name: Peter Pythons, nickname: Pyper, position: forward

Loading
Loading
You have reached the end of this section! Continue to the next section:

You can check your current points from the blue blob in the bottom-right corner of the page.