Web Analytics

Encapsulation

Intermediate ~25 min read

Encapsulation is the practice of bundling data and methods that operate on that data within a single class, while restricting direct access to some components. Python uses naming conventions (underscores) rather than strict access modifiers to indicate the intended visibility of attributes and methods.

Python's Access Levels

Prefix Name Meaning
attr Public Accessible from anywhere
_attr Protected Internal use, subclasses OK (convention)
__attr Private Name mangled to prevent accidental access

Public Attributes

Public attributes have no underscore prefix and can be freely accessed and modified from anywhere. They're part of the class's public interface.

Output
Click Run to execute your code

Start Public, Encapsulate Later

Python philosophy: start with public attributes. Add encapsulation (underscore) when you actually need validation or computed values. Don't over-engineer prematurely.

Protected Attributes (_single underscore)

A single underscore prefix signals "this is internal, use at your own risk." It's a convention that Python developers respect, though it's not enforced by the language.

Output
Click Run to execute your code

It's a Convention, Not Enforcement

Python won't stop you from accessing _protected attributes. The underscore is a signal to other developers: "this is an implementation detail that may change." Respect the convention in your own code.

Private Attributes (__double underscore)

Double underscore prefix triggers "name mangling" - Python renames the attribute to include the class name, making accidental access harder. Use this to prevent subclass attribute conflicts.

Output
Click Run to execute your code

Name Mangling: __attr becomes _ClassName__attr

Python doesn't truly hide private attributes. It renames __secret to _MyClass__secret. This prevents accidental name clashes in inheritance, but determined users can still access it. Privacy in Python is about intent, not enforcement.

Getter and Setter Methods

To control access to attributes, provide getter and setter methods. These allow validation, computed values, and read-only attributes.

Output
Click Run to execute your code

Python Has a Better Way: @property

Instead of explicit get_x() and set_x() methods, Python provides the @property decorator for cleaner syntax. You'll learn this in the Properties lesson.

Common Mistakes

1. Confusing _ and __ purposes

# Wrong - using __ just for "privacy"
class User:
    def __init__(self, name):
        self.__name = name  # Overkill! Use single underscore

# Better - single underscore for internal attributes
class User:
    def __init__(self, name):
        self._name = name  # Convention: internal use

# Use __ only to prevent subclass conflicts

2. Accessing private via name mangling

# Bad practice - bypassing name mangling
class Secret:
    def __init__(self):
        self.__value = 42

s = Secret()
print(s._Secret__value)  # Works but DON'T DO THIS!

# If you need to access it, the class should provide a method
# Or reconsider your design

3. Forgetting validation in setters

# Pointless getter/setter - no validation
class Person:
    def __init__(self, age):
        self._age = age

    def get_age(self):
        return self._age

    def set_age(self, age):
        self._age = age  # No validation! Why have a setter?

# Better - setter should validate
class Person:
    def set_age(self, age):
        if age < 0 or age > 150:
            raise ValueError("Invalid age")
        self._age = age

4. Returning mutable internal data

# Wrong - exposing internal list
class ShoppingCart:
    def __init__(self):
        self._items = []

    def get_items(self):
        return self._items  # Caller can modify!

cart = ShoppingCart()
items = cart.get_items()
items.append("hacked!")  # Modifies internal state!

# Correct - return a copy
def get_items(self):
    return self._items.copy()  # Safe copy

5. Over-encapsulating simple data classes

# Over-engineered for simple data
class Point:
    def __init__(self, x, y):
        self._x = x
        self._y = y

    def get_x(self): return self._x
    def get_y(self): return self._y
    def set_x(self, x): self._x = x
    def set_y(self, y): self._y = y

# Better - just use public attributes for simple data
class Point:
    def __init__(self, x, y):
        self.x = x  # Simple, direct
        self.y = y

Exercise: Secure User Account

Task: Create a User class with proper encapsulation for sensitive data.

Requirements:

  • Public: username - visible to everyone
  • Protected: _email - internal use with getter
  • Private: __password - never exposed directly
  • Methods: verify_password(), change_password()
  • __str__: Show username and masked email
Output
Click Run to execute your code
Show Solution
class User:
    def __init__(self, username, email, password):
        self.username = username      # Public
        self._email = email           # Protected
        self.__password = password    # Private

    def get_email(self):
        return self._email

    def verify_password(self, password):
        return password == self.__password

    def change_password(self, old_password, new_password):
        if self.verify_password(old_password):
            self.__password = new_password
            return True
        return False

    def __str__(self):
        # Mask email: first char + *** + @domain
        parts = self._email.split('@')
        masked = parts[0][0] + '***@' + parts[1]
        return f"User: {self.username}, Email: {masked}"


# Test
user = User("alice", "[email protected]", "secret123")
print(user.username)  # alice
print(user.get_email())  # [email protected]
print(user.verify_password("secret123"))  # True
print(user.verify_password("wrong"))  # False
print(user.change_password("secret123", "newpass"))  # True
print(user)  # User: alice, Email: a***@example.com

Summary

  • Public (no prefix): Accessible from anywhere, part of public API
  • Protected (_single): Convention for internal use, subclasses OK
  • Private (__double): Name mangled to prevent subclass conflicts
  • Name mangling: __attr becomes _ClassName__attr
  • Getters/setters: Methods for controlled access and validation
  • Python relies on conventions, not enforcement - "we're all consenting adults"

What's Next?

Now that you understand encapsulation, learn about Polymorphism - how different classes can share the same interface, enabling flexible and reusable code through duck typing and method overriding.