5.2 Encapsulation, Privacy, and Name Mangling

Key Takeaways

  • Python embraces the 'consenting adults' philosophy, relying on naming conventions and compiler transformations rather than rigid access modifiers like `private` or `protected`.
  • Public attributes have no leading underscores and constitute the object's supported public API.
  • Protected attributes are indicated by a single leading underscore (`_attr`); this is an advisory PEP 8 convention signaling internal implementation details that remain accessible at runtime.
  • Private attributes use double leading underscores (`__attr` without trailing underscores), triggering compiler name mangling into `_ClassName__attr` to prevent accidental subclass collisions.
  • Dunder attributes (`__init__`, `__dict__`) with trailing double underscores are strictly exempt from name mangling, and attributes can be introspected dynamically using `__dict__`, `hasattr()`, `getattr()`, `setattr()`, and `delattr()`.
Last updated: August 2026

Encapsulation, Privacy, and Name Mangling

Encapsulation is a core principle of object-oriented programming that bundles data (attributes) and the methods operating on that data into a cohesive unit while restricting direct outside access to internal implementation details. In languages like C++, Java, or C#, encapsulation is enforced through strict access specifiers (public, protected, private) enforced by the compiler. Python adopts a distinctly pragmatic approach governed by community conventions and syntactic transformations.


1. Encapsulation Philosophy: "Consenting Adults"

A famous guiding aphorism in the Python community, coined by Python creator Guido van Rossum, states:

"We are all consenting adults here."

This philosophy means Python does not erect impenetrable access barriers to prevent developers from inspecting or modifying an object's internal state. Instead, Python relies on clear naming conventions to signal design intent, combined with a compiler feature called name mangling to prevent accidental namespace collisions in inheritance hierarchies.


2. Public Attributes vs. Protected Convention (_attr)

Python classifies attributes into three primary visibility tiers based entirely on identifier prefix conventions:

+-----------------------+-----------------------------+----------------------------------------------+
| Visibility Category   | Naming Syntax               | Access & Meaning                             |
+-----------------------+-----------------------------+----------------------------------------------+
| Public                | name (e.g., self.host)      | Fully accessible; part of public interface.  |
| Protected (Convent.)  | _name (e.g., self._host)    | Advisory convention: internal implementation.|
| Private (Mangling)    | __name (e.g., self.__host)  | Name-mangled by compiler to _Class__name.    |
+-----------------------+-----------------------------+----------------------------------------------+

Public Attributes

Any attribute defined without leading underscores is public. It can be freely read, modified, and deleted from inside and outside the class:

class WebServer:
    def __init__(self, host, port):
        self.host = host  # Public attribute
        self.port = port  # Public attribute

srv = WebServer("0.0.0.0", 8080)
print(srv.host)     # 0.0.0.0
srv.port = 443      # Direct mutation permitted

Protected Attributes: Single Leading Underscore (_attr)

An attribute starting with a single leading underscore (and not ending in an underscore) indicates that the variable is intended for internal or protected use within the class and its subclasses.

  • Runtime Behavior: Python's runtime does not restrict access to _attr. Outside code can still read or write to obj._attr without encountering any errors.
  • Signaling Intent: Linters (like flake8, pylint), static type checkers (mypy), and IDE auto-complete recognize _attr as private, warning developers against calling it externally.
  • Module Wildcard Imports: Top-level module symbols starting with _ are excluded from wildcard imports (from module import *) unless explicitly enumerated in __all__.
class DatabasePool:
    def __init__(self):
        self._connection_pool = []  # Protected convention: internal state

    def _validate_connection(self, conn):  # Protected helper method
        return True

pool = DatabasePool()
# Accessible, but violates Python conventions and design contracts
pool._connection_pool.append("conn_1")

3. Private Attributes and Compiler Name Mangling (__attr)

When you prefix an attribute or method name with at least two leading underscores and at most one trailing underscore (e.g., __secret_key), Python triggers an automatic compiler transformation known as Name Mangling.

The Name Mangling Transformation Rule

Whenever the Python compiler parses an identifier of the form __varname inside a class block named ClassName, it rewrites the identifier into: _ClassName__varname

class BankVault:
    def __init__(self, initial_code):
        self.__access_code = initial_code  # Mangled to _BankVault__access_code

    def verify_code(self, code):
        # Inside the class, self.__access_code is automatically rewritten
        return self.__access_code == code

vault = BankVault("Secret-9942")

# Verification method works seamlessly
print(vault.verify_code("Secret-9942"))  # True

Direct Access Raises AttributeError

If external code attempts to access vault.__access_code directly, Python raises an AttributeError because the identifier __access_code does not exist in the instance dictionary:

try:
    print(vault.__access_code)
except AttributeError as err:
    print("Direct access blocked:", err)
    # Output: Direct access blocked: 'BankVault' object has no attribute '__access_code'

Accessing the Mangled Attribute

Because name mangling is a syntactic transformation rather than a security firewall, the attribute can still be accessed from the outside using its mangled identifier _ClassName__attr:

# Accessing via the mangled name succeeds
print(vault._BankVault__access_code)  # Secret-9942

# Modifying via the mangled name also succeeds
vault._BankVault__access_code = "New-Code-0000"
print(vault.verify_code("New-Code-0000"))  # True

Purpose of Name Mangling: Subclass Collision Avoidance

The primary engineering purpose of name mangling is not security, but preventing accidental attribute overrides in subclasses. If a base class and a derived subclass both define private helper attributes (e.g., __setup()), name mangling ensures they resolve to _Base__setup and _Derived__setup respectively, eliminating collisions.


4. The Trailing Underscore Distinction and Dunder Exceptions

The exact placement and count of underscores dictate how Python parses the identifier:

A. Special Dunder Identifiers (__name__)

Identifiers starting with double underscores and ending with double underscores (known as dunders or magic attributes, such as __init__, __doc__, __dict__, __str__) are strictly exempt from name mangling:

class Sample:
    def __custom__(self):
        return "Not mangled"

s = Sample()
# __custom__ is NOT mangled because of the trailing double underscores
print(s.__custom__())  # 'Not mangled'
print(hasattr(s, '__custom__'))  # True

B. Keyword Disambiguation (name_)

A single trailing underscore (e.g., class_, def_, in_, type_) is used by convention to avoid naming conflicts with reserved Python keywords:

def filter_items(items, class_=None):
    """'class_' avoids colliding with the reserved keyword 'class'."""
    pass

5. Attribute Reflection and Namespace Inspection (__dict__)

In Python, namespaces are implemented as standard dictionaries. You can inspect and manipulate an object's namespace directly or via built-in reflection functions.

The __dict__ Attribute

Every regular Python class and instance maintains a __dict__ dictionary holding its writable attributes:

class User:
    role = "Standard"  # In User.__dict__

    def __init__(self, username, pin):
        self.username = username  # In self.__dict__
        self._status = "Active"    # In self.__dict__
        self.__pin = pin          # In self.__dict__ as '_User__pin'

u = User("alex", 1234)

print(u.__dict__)
# Output: {'username': 'alex', '_status': 'Active', '_User__pin': 1234}

print(User.__dict__.keys())
# Includes: 'role', '__init__', '__dict__', '__doc__', etc.

Built-in Reflection Functions

Python provides four built-in functions for dynamic attribute inspection, retrieval, modification, and deletion:

FunctionSignaturePurpose & Error Handling
hasattr()hasattr(object, name)Returns True if name (string) exists on object (or in its class hierarchy); otherwise False.
getattr()getattr(object, name[, default])Retrieves attribute name. Raises AttributeError if missing, unless default is supplied.
setattr()setattr(object, name, value)Assigns value to attribute name on object, equivalent to object.name = value.
delattr()delattr(object, name)Deletes attribute name from object. Raises AttributeError if missing.
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

pt = Point(10, 20)

# 1. Inspecting attribute existence
print(hasattr(pt, 'x'))       # True
print(hasattr(pt, 'z'))       # False

# 2. Dynamic attribute retrieval with safe fallback
print(getattr(pt, 'y'))            # 20
print(getattr(pt, 'z', 0))         # 0 (fallback default)

# 3. Dynamic attribute setting
setattr(pt, 'z', 30)          # Equivalent to pt.z = 30
print(pt.z)                   # 30

# 4. Dynamic attribute deletion
delattr(pt, 'x')              # Equivalent to del pt.x
print(hasattr(pt, 'x'))       # False

Exam Trap: hasattr(), getattr(), setattr(), and delattr() require the attribute name to be passed as a string (e.g., getattr(pt, 'x')), not as a raw variable identifier (getattr(pt, x)).

Loading diagram...
Compiler Name Mangling Transformation and Runtime Namespace
Test Your Knowledge

A Python developer defines the following class:

class SecretVault:
    def __init__(self, key):
        self.__key = key

vault = SecretVault("alpha-9")
Under what exact key name is the __key attribute stored inside vault.__dict__?

A
B
C
D
Test Your Knowledge

Consider the following code snippet:

class Device:
    def __init__(self, model):
        self.__model = model

d = Device("Router-X")
print(hasattr(d, '__model'), hasattr(d, '_Device__model'))
What is printed to the console when this script runs?

A
B
C
D
Test Your Knowledge

A programmer attempts to access a private attribute directly from outside the class definition:

class Payment:
    def __init__(self, amount):
        self.__amount = amount

p = Payment(250)
print(p.__amount)
What occurs when the final line of code executes?

A
B
C
D
Test Your Knowledge

Which of the following method or attribute names is EXEMPT from Python compiler name mangling when defined inside a class?

A
B
C
D