Encapsulation and Private Attributes in Python
Explore encapsulation in Python by learning how to protect sensitive data within classes using private attributes and controlled methods. Understand how to hide implementation details using conventions like leading underscores to safeguard information such as passwords and like counts. This lesson teaches you to maintain data integrity and abstraction while working on practical challenges in object-oriented programming.
We'll cover the following...
We’ve made great progress with creating users and posts, but now we face a crucial question:
How do we protect sensitive information or hide implementation details from others and ensure that other parts of the system do not expose or tamper with certain data?
A user’s password or a post’s likes count should not be freely accessible to others who interact with the system. Direct access to these kinds of data could lead to security risks or inconsistencies in how the data is used. For example, if anyone could modify a user’s password directly or change a post likes count, the integrity of the system could be compromised.
This brings us to encapsulation, a core principle in object-oriented programming that helps us manage and protect data within our objects. Encapsulation bundles the data and the methods that operate on it together while also restricting access to certain parts of the object’s internal state.
Before starting out, let’s review the key ideas:
Encapsulation: This is the principle of bundling data and the methods that operate on that data into a single unit (the object). In our project, we want each object (such as a User or a Post) to manage its own data.
Data hiding/Private attributes: In Python, we use a leading underscore (e.g., _password or _like_count) to indicate that an attribute is meant to be private. However, it’s often stated that private data members are used for data protection, but this isn’t entirely accurate. The main purpose of private data members isn’t to secure data but rather to support the principle of _password directly.
In the context of Chirpy:
For the
Userclass, we want to hide sensitive information like the user’s password.For the
Postclass, we might want to hide internal details such as a like count that should only be modified by controlled methods.
Enhancing the User class for sensitive data
Let’s begin by updating our User class. We need to store a password for each user.
class User:total_users = 0def __init__(self, username, display_name, password):self.username = usernameself.display_name = display_nameself.password = password # Storing the password as a public attributeself.posts = [ ]User.total_users += 1def show_profile(self):print("User:", self.display_name, "(@" + self.username + ")")
At first glance, this works perfectly—we can create a user and even print the profile. But later on, when someone tries to access the password directly, we realize it’s a problem.
For instance:
from post import Post
import random
class User:
total_users = 0
def __init__(self, username, display_name, password):
self.username = username
self.display_name = display_name
self.password = password # Mistake: password is public
self.posts = [ ]
User.total_users += 1
def show_profile(self):
print("User:", self.display_name, "(@" + self.username + ")")
def create_post(self, content):
post = Post(content, self)
self.posts.append(post)
return post
def like_post(self, post):
post.like_post(self)
# Class method to create a guest user with a random username
@classmethod
def create_guest_user(cls):
random_number = random.randint(100, 999)
username = "guest" + str(random_number)
display_name = "Guest" # Default display name
return cls(username, display_name)Oops! This reveals the user’s password to any part of our program, which is not secure. In Python, by convention, we signal that an attribute is meant to be private by prefixing it with an underscore “_”. Here, Post or even any other class that will have a User object will be able to access passwords this way.
In most OOP languages, we can define truly private attributes, which cannot be accessed outside the class. However, Python does not have strict enforcement of this concept. Instead, Python relies on a convention: we mark an attribute as private by prefixing it with a leading underscore (_), like this:
self._password = password
This convention signals to other developers that this attribute is intended to be private, meaning they should not access it directly. However, Python does not prevent access to the attribute; it’s up to the developers to follow this convention.
But there are some limitations to it as well:
No true privacy: Python does not have strict access control for attributes (like Java or C++). So, even if we prefix an attribute with an underscore (e.g.,
_password), it’s still technically accessible. The underscore simply warns developers not to interact with the attribute directly.Developer responsibility: Because Python doesn’t enforce strict privacy, developers are expected to respect these conventions. This means we can’t rely on Python to automatically prevent access to private data; instead, we trust other developers to follow best practices.
Now, let’s add this along with some additional functionality for safely handling the user’s password in our User class!
Now, even though the attribute isn’t truly hidden (Python relies on conventions rather than strict access control), other developers know not to access _password directly.
Let’s see how it works:
from post import Post
import random
class User:
total_users = 0
def __init__(self, username, display_name, password):
self.username = username
self.display_name = display_name
self._password = password # Using a leading underscore to indicate privacy
self.posts = [ ]
User.total_users += 1
def show_profile(self):
print("User:", self.display_name, "(@" + self.username + ")")
def create_post(self, content):
post = Post(content, self)
self.posts.append(post)
return post
def like_post(self, post):
post.like_post(self)
# Class method to create a guest user with a random username
@classmethod
def create_guest_user(cls):
random_number = random.randint(100, 999)
username = "guest" + str(random_number)
display_name = "Guest" # Default display name
return cls(username, display_name)
def change_password(self, new_password):
# Only allow password change if new password is at least 6 characters long
if len(new_password) >= 6:
self._password = new_password
print("Password updated successfully!")
else:
print("Error: Password must be at least 6 characters long.")
def check_password(self, input_password):
return input_password == self._passwordNow, when we run the above code,
_passwordis still accessible (Python doesn’t enforce privacy), but the underscore tells other developers, “Do not access this directly.” as it is a private attribute.
Challenge: Securing the like count in the Post class
Enhance the Post class to securely track the number of likes using encapsulation. Your task is to ensure the like count is only modified through controlled methods and not directly accessible from outside the class.
In the current
Postclass, thelikeslist is public, and the like count is not securely tracked. This can lead to inconsistencies if the like count is modified directly. Your goal is to:
Add a private attribute
_like_countto track the number of likes.Ensure the like count is only updated through the
like_post()method.Provide a public method
get_like_count()to safely retrieve the like count.
Here’s the current Post class:
After completing the challenge, the following output should be produced:
Alex liked this post!Private like count (via getter): 1
Now add your solution in the widget below. We have already added the main class code for your ease!
from post import Post
import random
class User:
total_users = 0
def __init__(self, username, display_name, password):
self.username = username
self.display_name = display_name
self._password = password # Using a leading underscore to indicate privacy
self.posts = [ ]
User.total_users += 1
def show_profile(self):
print("User:", self.display_name, "(@" + self.username + ")")
def create_post(self, content):
post = Post(content, self)
self.posts.append(post)
return post
def like_post(self, post):
post.like_post(self)
@classmethod
def create_guest_user(cls):
random_number = random.randint(100, 999)
username = "guest" + str(random_number)
display_name = "Guest"
return cls(username, display_name)
def change_password(self, new_password):
# Only allow password change if new password is at least 6 characters long
if len(new_password) >= 6:
self._password = new_password
print("Password updated successfully!")
else:
print("Error: Password must be at least 6 characters long.")
def check_password(self, input_password):
return input_password == self._passwordToday, we learned how to protect sensitive data by:
Hiding attributes (using a leading underscore) to signal that they are private.
Ensuring data integrity by only modifying these attributes through controlled methods.
What’s next?
Our improved design keeps the internal state secure and makes our code more organized. Next, we’ll explore how to organize all of our classes into a unified structure—a SocialNetwork class that acts as the home base for our app. Imagine managing thousands of users and posts in one place!
Happy coding, and see you in the next lesson!