Name Mangling — Deep Dive

How Name Mangling Works at the Bytecode Level

Name mangling is a compile-time transformation, not a runtime one. When Python’s compiler (the compile() function) processes a class body, it scans for identifiers starting with __ (but not ending with __) and prepends _ClassName to them.

You can verify this by disassembling bytecode:

import dis

class Example:
    def method(self):
        self.__secret = 42
        return self.__secret

dis.dis(Example.method)

The bytecode shows STORE_ATTR and LOAD_ATTR for _Example__secret, not __secret. The transformation is baked into the compiled code object.

The Compiler’s Perspective

Inside CPython’s symtable.c, the _Py_Mangle function handles this:

// Simplified pseudocode of CPython's mangling logic
if (name starts with "__" && !name ends with "__" && class_name is not empty):
    mangled = "_" + class_name.lstrip("_") + name

Note the lstrip("_") on the class name. If your class is __Weird, the mangled form of __attr becomes _Weird__attr (stripping leading underscores from the class name).

Edge Cases and Surprises

Mangling Inside Nested Classes

class Outer:
    class Inner:
        def __init__(self):
            self.__x = 1  # Becomes _Inner__x, NOT _Outer__x

    def method(self):
        self.__y = 2  # Becomes _Outer__y

Mangling uses the immediately enclosing class name, not any outer class.

Mangling in Comprehensions

class Foo:
    __items = [1, 2, 3]

    # This works — __items is mangled to _Foo__items in the class body
    doubled = [x * 2 for x in __items]

But there’s a subtle CPython implementation detail: in Python 3, comprehensions create their own scope. The __items reference inside the comprehension is resolved during class body execution where mangling applies, so it works. However, the iteration variable itself isn’t mangled.

Mangling and exec/eval

class Dynamic:
    def make_attr(self):
        exec("self.__dynamic = 99")
        # This creates self.__dynamic, NOT self._Dynamic__dynamic
        # Because exec() doesn't know about the enclosing class at compile time

Since exec compiles code at runtime without class context, no mangling occurs. This is a common gotcha when generating attribute access dynamically.

Mangling in Dataclasses

from dataclasses import dataclass

@dataclass
class Config:
    __secret: str = "default"  # Becomes _Config__secret

# The generated __init__ uses the mangled name
c = Config()
print(c._Config__secret)  # "default"
# print(c.__secret)  # AttributeError

Dataclasses respect mangling, but the auto-generated __init__ parameter is also mangled, which makes the constructor awkward to use. This is generally a sign you shouldn’t use double underscores with dataclasses.

Real-World Collision Scenario: Framework Design

Consider a web framework’s base view class:

class BaseView:
    def __init__(self):
        self.__cache = {}

    def dispatch(self, request):
        cache_key = request.path
        if cache_key in self.__cache:
            return self.__cache[cache_key]
        response = self.handle(request)
        self.__cache[cache_key] = response
        return response


class UserView(BaseView):
    def __init__(self):
        super().__init__()
        self.__cache = {}  # User-level cache — doesn't collide!

    def handle(self, request):
        if request.user_id in self.__cache:
            return self.__cache[request.user_id]
        user = self.load_user(request.user_id)
        self.__cache[request.user_id] = user
        return user

Without mangling, both __cache attributes would be the same dict, causing subtle bugs where the view-level cache and user-level cache contaminate each other. With mangling, _BaseView__cache and _UserView__cache are completely separate.

Name Mangling vs. Access Control in Other Languages

FeaturePythonJavaC++
True private (compile-enforced)NoYes (private)Yes (private)
Name manglingYes (double underscore)No (not needed)Yes (C++ name mangling for linker symbols)
Convention-based privateYes (single underscore)No (has real private)No
Reflection bypassTrivial (_Class__attr)Possible but requires setAccessible(true)Not standard

Python’s approach reflects its “we’re all consenting adults” philosophy. The language provides tools to signal intent, not walls to enforce it.

Testing and Debugging with Mangled Names

Accessing Mangled Attributes in Tests

import pytest

class TestWidget:
    def test_internal_state(self):
        w = Widget()
        w.process()

        # Option 1: Access the mangled name directly
        assert w._Widget__internal_counter == 1

        # Option 2: Use a public method instead (preferred)
        assert w.get_counter() == 1

        # Option 3: Use vars() to inspect all attributes
        attrs = vars(w)
        assert "_Widget__internal_counter" in attrs

Finding All Mangled Attributes

def find_mangled_attrs(obj):
    """Find all name-mangled attributes on an object."""
    return {
        name: value
        for name, value in vars(obj).items()
        if name.startswith("_") and "__" in name[1:]
        and not name.endswith("__")
    }

The dir() Perspective

dir(obj) shows mangled names in their transformed form:

class Secret:
    def __init__(self):
        self.__hidden = 42

s = Secret()
print([attr for attr in dir(s) if "hidden" in attr])
# ['_Secret__hidden']

This is useful for debugging — dir() reveals the actual attribute names stored on the object.

Performance Impact

Name mangling has zero runtime performance cost. The transformation happens at compile time. The mangled name is stored directly in the bytecode, and attribute access uses the mangled name with the same mechanism as any other attribute. There’s no lookup indirection or extra computation.

The only “cost” is slightly longer attribute names in memory (_ClassName__attr vs __attr), which is a few extra bytes per attribute — completely negligible.

Best Practices for Library Authors

  1. Use double underscore for truly internal implementation details that subclasses must not override — internal caches, state machines, lock objects.
  2. Use single underscore for “protected” attributes that subclasses might reasonably need to access or override.
  3. Document the intent. Even with mangling, document whether an attribute is part of the class’s internal contract or an implementation detail subject to change.
  4. Don’t over-mangle. If you’re writing application code (not a reusable library), single underscores are almost always sufficient. Name mangling solves a framework-level problem.
  5. Never use mangling for security. It’s trivially bypassed. For actual secret data, use proper encryption or don’t store it as an attribute.

One thing to remember: Name mangling is a compile-time transformation that prevents accidental attribute collisions in inheritance hierarchies. It’s not privacy, not security, not access control — it’s namespace isolation for class internals. Use it when designing libraries and frameworks where subclass authors shouldn’t accidentally shadow your internal state.

pythonadvancedoop

See Also