What is Python Encapsulation?

Encapsulation means restricting the access of methods and variables. We add the direct access and change restriction feature to methods or variables. So why are we doing this? Because the codes we write should not be changed or the values we change should be changed in a controlled manner. It is in these situations that encapsulation comes to our rescue like a magic wand. Now let’s show with examples.

class RegisterCourse:     
    def __init__(self):
        self.name = "Baransel"
        self.surname = "Arslan"
        self.exam1 = 82
        self.exam2 = 93

register = RegisterCourse()

print("Name : ", register.name)
print("Surname : ", register.surname) 
print("Exam 1 : ", register.exam1)
print("Exam 2 :", register.exam2)

We created a class called RegisterCourse. Now let’s view the variables of our class

Name :  Baransel
Surname :  Arslan
Exam 1 :  82
Exam 2 : 93

There is only one problem here. We can access and even change this student’s information from outside.

register.exam1 = 40
register.exam2 = 50

Let’s look at the information again;

Name :  Baransel
Surname :  Arslan
Exam 1 :  40
Exam 2 : 50

As you can see, we can access and change the student’s personal information. So, how can we restrict this person’s information from being accessed and changed from outside?

Using Python Encaplusation

Since we create the person’s information globally, it is accessible to everyone. For this, we need to define our data and methods as private. Let’s see how it’s done.

It’s a simple process, we just add __ in front of the data we created, let’s try it right away;

class RegisterCourse:     
    def __init__(self):
        self.name = "Baransel"
        self.surname = "Arslan"
        self.__exam1 = 82
        self.__exam2 = 93

register = RegisterCourse()

print("Name : ", register.name)
print("Surname : ", register.surname) 
print("Exam 1 : ", register.exam1)
print("Exam 2 :", register.exam2)

We got an error like this, right?

AttributeError: 'RegisterCourse' object has no attribute 'exam1'

Since we made the exam information private, it gave an error because it could not reach the exam information. Now we can access these values only within this class. So how do we access it from other classes? Let’s continue reading the article. So, can we do the same functions in methods? Let’s show it with an example.

class RegisterCourse:     
    def __init__(self):
        self.name = "Baransel"
        self.surname = "Arslan"
        self.__exam1 = 82
        self.__exam2 = 93
        self.courses = []
    def add(self, course):
        self.courses.append(course)

register = RegisterCourse()

register.add("Database Managment")
print("Name : ", register.name)
print("Surname : ", register.surname) 
print(register.courses)

As you can see, we can see the courses taken by the student and add courses. Now let’s restrict external access and modification in this method.

class RegisterCourse:     
    def __init__(self):
        self.name = "Baransel"
        self.surname = "Arslan"
        self.__exam1 = 82
        self.__exam2 = 93
        self.courses = []
    def __add(self, course):
        self.courses.append(course)

register = RegisterCourse()

register.add("Database Managment")

AttributeError: 'RegisterCourse' object has no attribute 'add'

Again we got a similar error. Although we have a method called add(), we cannot access it because there is no outside access (we did this by adding __ to the beginning of the method).

Well, we can think of this. Ok, I have blocked my external access to exam1 and exam2 variables. Now I can’t both read and change exam1 and exam2 variables. But I want to at least read the value, if I don’t change it.

External Access to Encapsulated Data

So how do we do this? We will do it with a structure similar to the getter and setter methods, which we often encounter in almost many programming languages, then let’s try it.

class RegisterCourse:     
    def __init__(self):
        self.name = "Baransel"
        self.surname = "Arslan"
        self.__exam1 = 82
        self.__exam2 = 93
        self.__courses = []

    def __add(self, course):
        self.__courses.append(course)
    
    def getExam1(self):
        return self.__exam1

register = RegisterCourse()

print("Name : ", register.name)
print("Surname : ", register.surname) 
print("Exam 1 : ", register.getExam1())

Let’s take a look at the Output:

Name :  Baransel
Surname :  Arslan
Exam 1 :  82

As you can see, we have reached the exam1 information in this way. By the way, I created the name getExam1 and you can give it another name. Since this structure is a general structure, writing it this way (putting get at the beginning of the variable) can increase the readability of your code.

Well, let’s say that even though the variable is private, I want to both access and change its value. We just added a getter method, now let’s add a setter method.

class RegisterCourse:     
    def __init__(self):
        self.name = "Baransel"
        self.surname = "Arslan"
        self.__exam1 = 82
        self.__exam2 = 93
        self.__courses = []

    def __add(self, course):
        self.__courses.append(course)
    
    def getExam1(self):
        return self.__exam1
    
    def setExam1(self, newValue):
        self.__exam1 = newValue

register = RegisterCourse()

print("Name : ", register.name)
print("Surname : ", register.surname) 
print("Exam 1 : ", register.getExam1())

Let’s take a look at the output;

Name :  Baransel
Surname :  Arslan
Exam 1 :  82

As you can see the grade information has not changed. Because we didn’t call the setExam1() function, let’s call the setExam1() function we just created.

register.setExam1(60)

Let’s look at the output;

Name :  Baransel
Surname :  Arslan
Exam 1 :  60

This is how we call our function. You can now change private values with the setter function.

Well, I can’t access it directly, but I was able to change the variable as I wanted with the setter method. Why bother with the getter setter? Why did I feel the need to make it private and use the setter method? Because when I access it directly, I can change the value of the variable without any checking. But when I use the setter function in this way, I can put the validations I want in it. For example, in direct access to exam1 value, I can give – (negative) values, but when a negative value comes in the setter method, I can warn the user or throw an error without directly equating it to exam1 value. Let’s show it with an example;

def setExam(self, newValue):
    if newValue<0 or newValue>100 :
        raise ValueError("Exam score cannot be less than 0 and greater than 100. ")

We wrote our setter function, now let’s see our whole program.

class RegisterCourse:     
    def __init__(self):
        self.name = "Baransel"
        self.surname = "Arslan"
        self.__exam1 = 82
        self.__exam2 = 93
        self.__courses = []

    def __add(self, course):
        self.__courses.append(course)
    
    def getExam1(self):
        return self.__exam1
    
    def setExam(self, newValue):
        if newValue<0 or newValue>100 :
            raise ValueError("Exam score cannot be less than 0 and greater than 100. ")
        self.__exam1 = newValue

register = RegisterCourse()
register.setExam(-10)

ValueError: Exam score cannot be less than 0 and greater than 100.

Here, we have checked whether the exam score is less than 0 and greater than 100.