In object-oriented programming, encapsulation is a mechanism of  limiting access to variables and methods from outside the class they are defined in.  It helps in maintaining the integrity of an object's internal state by preventing outside code from directly manipulating the object's data. Encapsulation also helps to hide the inner workings and details of a class from the outside world.

In some languages such as C++ and Java, it is possible to define properties and methods that are only accessible only from within the class.  These properties and methods are known as private members. Python, however,  does not have the concept of private members. All attributes and methods are public and can be accessed from outside the class.

To accomplish encapsulation in Python, we use a convention of prefixing the name of the member with a single underscore (_).  For example:

#Define the class
class Demo:
    def __init__(self):
       #define a private variable with a leading underscore
       self._private = "BlahBlahBlah"

    #Define a private method
    def _private_method(self):
       print("I am private")

#create an object from the class Demo
d = Demo()

#The private  variables and methods are still accessible from outside the class, as shown below.
print(d._private)
d._private_method()

Note: Double underscores  are reserved for special system methods also known as Dunder(Double Underscore) method, such as the familiar __init__ method, the __len__ method, etc.

Let us see an example that is a little bit more relevant.  A stack is a data structure that follows the Last In First Out (LIFO) approach. The most relevant methods in a stack are. 

  1. push – Add an item to the stack
  2. pop – Remove the top item of the stack
  3. top – Check what the top item of the stack is
  4. is_empty – Check if the stack is empty
  5. size – Check the size of the stack

We can implement a Stack class by using a list as the storage for the Queue items.

#define the class
class Stack:
   def __init__(self):
       #define some private attribute
       self._data = []
       self._size = 0
    
   #Define the methods relevant to a stack dat types
   def push(self, v):
      self._size += 1
      self._data.append(v)
   
   def pop(self):
       if self.is_empty():
           raise(Exception("Stack Is Empty!"))
       self._size -= 1
       return self._data.pop()
 
   def top(self):
       return self._data[-1]

   def size(self):
       return self._size

   def is_empty(self):
       return self.size() == 0

S = Stack()
S.push(10)
S.push(20)
S.push(30)

print(S.is_empty())
print(S.top())

print(S.pop())
print(S.pop())
print(S.pop())
print(S.pop())

Illustration from a builtin class

We can use the builtin dir() function to view the methods and attributes that are associated with a class. For example, there is a builtin module called queue which defines a class called Queue . let us use the dir function to view the private variables defined in the class. We will filter the unnecessary dunder methods from the returned list.

#import class Queue from the builtin module queue
from queue import Queue

#print attributes and methods of the class excluding dunder method
print([i for i in dir(Queue) if not i.startswith('__')])

As you can see above, the class defines three private attributes i.e  '_get', '_init', '_put' . A person interacting with the class will immediately know that these attributes are not meant for public interaction or use.

Why use the single underscore for private members?

The following list illustrates reasons why we use single underscore for private members despite the fact that the members still remain accessible from outside the class:

  • To improve code organization and readability.
  • To indicate a particular class attribute is for internal use only and the user should not interact with it directly.
  • To allow polymorphism where an attribute can be given different values according to the specific class instance.

Summary

  1. Attributes and methods in a class defined without a leading underscore are public. Such methods or attribute can be used freely.
  2. Attributes and methods with a single leading underscore are private and should not be interacted with directly.
  3. Attributes and methods with double underscore are for special system purposes . They are mainly used by the Python interpreter and are also known as "magic methods". or "dunder methods"