1. Introduction: What Is Encapsulation?
In Object-Oriented Programming (OOP), encapsulation is one of the most critical foundational concepts.
Encapsulation means bundling all of an object's data attributes and the methods that operate on those attributes together inside the object itself. Instead of exposing internal variables directly to the outside world, encapsulation provides external users and programs with dedicated methods to interact with and manipulate those attributes safely.
2. The Concept of Data Hiding (The Laptop Analogy)
A core component of encapsulation is data hiding—deliberately concealing internal implementation details and raw data structures from the user.
Consider a modern computer or laptop:
- Inside a laptop lies complex electronic circuitry, chips, motherboards, and wiring.
- Manufacturers do not expose these delicate internal circuits to the user. Instead, they enclose the circuitry inside a protective outer casing and provide simple external interfaces: a screen, a keyboard, and a mouse.
- A user operates the entire computer using only these exposed interfaces without ever needing to touch or inspect the internal wiring.
In Python OOP, the same design principle applies: we hide internal variables inside the class and expose only specific methods to the outside world.
3. Public vs. Private Attributes
In earlier topics, variables defined inside classes were public attributes, meaning they could be read and modified directly from the global scope outside the class.
However, good software architecture requires that certain attributes remain private:
- Private attributes: Data attributes that can only be accessed and utilized by methods inside their own class. They cannot be referenced, modified, or called directly from the global program scope.
- The double-underscore rule (
__): In Python, you declare an attribute as private by prefixing its name with two leading underscores (__).
self.name → Public attribute (accessible from outside).
self.__grade → Private attribute (strictly restricted to internal class methods).
4. Practical Jupyter Notebook Implementation: The Student Class
Let's implement a Student class where a student's name is public, but their grade is kept private using double underscores:
class Student:
# Constructor initializing public and private attributes
def __init__(self, name, grade):
self.name = name # Public attribute
self.__grade = grade # Private attribute (prefixed with double underscores)
# Public method exposed to safely access the private attribute
def get_grade(self):
return self.__grade
5. Attempting Direct Access: The AttributeError
If you instantiate an object from the Student class and attempt to access __grade directly from outside the class:
# 1. Instantiating student1 with name "Ali" and grade 90
student1 = Student("Ali", 90)
# 2. Accessing the public attribute
print("Student Name:", student1.name)
# 3. Attempting to directly access the private attribute
print(student1.__grade)
Execution and Output:
Student Name: Ali
---------------------------------------------------------------------------
AttributeError: 'Student' object has no attribute '__grade'
Why Does Python Say the Attribute Doesn't Exist?
Although __grade exists in memory as a private variable, Python raises an AttributeError stating that 'Student' object has no attribute '__grade'.
Python deliberately masks the existence of private attributes from the outside world rather than displaying an error message about permissions. For the outside program, the attribute simply does not exist directly.
6. Accessing Private Data Through Class Methods
Because private attributes cannot be reached directly, the outside program must call an exposed class method (such as get_grade()) to retrieve or interact with that data:
# Instantiating student objects
student1 = Student("Ali", 90)
student2 = Student("Sara", 95)
# Accessing private grade data safely via the public get_grade() method
print(f"{student1.name}'s Grade:", student1.get_grade())
print(f"{student2.name}'s Grade:", student2.get_grade())
Output:
Ali's Grade: 90
Sara's Grade: 95
By calling student1.get_grade(), the program accesses the internal __grade attribute from within the class's own method, returning the value 90 cleanly without raising an error. Similarly, calling it on student2 retrieves 95.