Search⌘ K
AI Features

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’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.

Encapsulation
Encapsulation

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 abstractionabstraction—an essential pillar of object-oriented programming (OOP). OOP emphasizes that, from the user’s perspective, only the external behavior of an object matters, not its internal implementation details, such as data structures or algorithms. Making data members private helps enforce this abstraction. If internal data were directly accessible, it would expose underlying implementation details, violating the principle. Even though the attribute isn’t truly hidden—since Python relies on conventions rather than strict access control—other developers understand that they shouldn't access _password directly.

In the context of Chirpy:

  • For the User class, we want to hide sensitive information like the user’s password.

  • For the Post class, 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 = 0
def __init__(self, username, display_name, password):
self.username = username
self.display_name = display_name
self.password = password # Storing the password as a public attribute
self.posts = [ ]
User.total_users += 1
def show_profile(self):
print("User:", self.display_name, "(@" + self.username + ")")
Storing password as public attribute in User class

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)
Storing password as public attribute in User class

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!

Python
class User:
total_users = 0
def __init__(self, username, display_name, password):
self.username = username
self.display_name = display_name
self._password = password # Correct: 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 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._password

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._password
Storing password as private attribute in User class

Now, when we run the above code, _password is 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 Post class, the likes list 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:

  1. Add a private attribute _like_count to track the number of likes.

  2. Ensure the like count is only updated through the like_post() method.

  3. Provide a public method get_like_count() to safely retrieve the like count.

Here’s the current Post class:

Python
class Post:
total_posts = 0
def __init__(self, content, author):
self.content = content
self.author = author
self.likes = [ ] # Public list of users who liked the post
Post.total_posts += 1
def like_post(self, user):
if user not in self.likes:
self.likes.append(user)
print(user.display_name, "liked this post!")
else:
print(user.display_name, "has already liked this post!")
@staticmethod
def validate_content_length(content):
if len(content) <= 280:
return True
else:
return False

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._password
Challenge: Securing the like count in the Post class

Today, 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!