Skip to content

How to Enforce Attribute Privacy in Python

How Python handles private and protected attributes using _, __, and @property, with examples for name mangling, controlled access, and validation.

Python's philosophy on data privacy is distinct from languages like Java or C++. Instead of strict private or protected keywords, Python relies on naming conventions and encapsulation mechanisms to signal intent and control access. The mantra is "We are all consenting adults here," meaning privacy is advisory rather than mandatory.

This guide explains how to use naming conventions, property decorators, and __slots__ to enforce attribute privacy effectively.

Understanding Python's Privacy Model

In Python, there is no way to make an attribute strictly inaccessible from outside the class. However, you can make it harder to access accidentally.

  • Public (name): Accessible everywhere.
  • Protected (_name): Convention only. Signals "internal use."
  • Private (__name): Name mangling applied. Harder to access accidentally.

Method 1: Protected Attributes (Single Underscore)

Prefixing an attribute with a single underscore _ signals to other developers (and IDEs/linters) that it is intended for internal use only. It does not prevent access technically.

class DatabaseConnection:
    def __init__(self):
        self.host = "localhost"      # Public
        self._password = "secret123" # Protected (Convention)

db = DatabaseConnection()

print(db.host)

print(db._password)

Output:

localhost
secret123

Method 2: Private Attributes (Double Underscore / Mangling)

Prefixing an attribute with two underscores __ (and no trailing underscores) triggers Name Mangling. Python internally renames the attribute to _ClassName__attributeName. This prevents accidental collisions in subclasses and makes direct access harder.

class BankAccount:
    def __init__(self, balance):
        self.__balance = balance # Private

    def get_balance(self):
        return self.__balance

account = BankAccount(1000)

print(f"Balance: {account.get_balance()}")

try:
    print(account.__balance)
except AttributeError as e:
    print(f"Error: {e}")

print(f"Mangled Access: {account._BankAccount__balance}")

Output:

Balance: 1000
Error: 'BankAccount' object has no attribute '__balance'
Mangled Access: 1000

Method 3: Using Property Decorators (Encapsulation)

The most robust way to "enforce" privacy logic (like read-only access or validation) is using properties. You store the data in a protected/private attribute but expose it via @property.

class User:
    def __init__(self, username):
        self._username = username # Internal storage

    @property
    def username(self):
        return self._username

    @username.setter
    def username(self, new_name):
        if not new_name:
            raise ValueError("Username cannot be empty")
        self._username = new_name

user = User("Alice")

print(user.username)

try:
    user.username = ""
except ValueError as e:
    print(f"Error: {e}")

Output:

Alice
Error: Username cannot be empty

Conclusion

To enforce attribute privacy in Python:

  1. Use _attribute for internal implementation details (Standard Convention).
  2. Use __attribute only to prevent name collisions in inheritance scenarios (Name Mangling).
  3. Use @property to control access logic (Read-only, Validation) while keeping a clean public API.
Navigation