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
| Feature | Python | Java | C++ |
|---|---|---|---|
| True private (compile-enforced) | No | Yes (private) | Yes (private) |
| Name mangling | Yes (double underscore) | No (not needed) | Yes (C++ name mangling for linker symbols) |
| Convention-based private | Yes (single underscore) | No (has real private) | No |
| Reflection bypass | Trivial (_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
- Use double underscore for truly internal implementation details that subclasses must not override — internal caches, state machines, lock objects.
- Use single underscore for “protected” attributes that subclasses might reasonably need to access or override.
- 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.
- 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.
- 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.
See Also
- Python Abc Abstract Base Classes Why Python's ABC module is like a building inspector who checks your blueprints before construction begins
- Python Class Decorators Understand Class Decorators through an everyday analogy so Python behavior feels intuitive, not random.
- Python Composition Vs Inheritance Understand Composition Vs Inheritance through an everyday analogy so Python behavior feels intuitive, not random.
- Python Cooperative Multiple Inheritance Why Python classes can have multiple parents and still get along — like a kid learning different skills from each family member.
- Python Dataclasses Advanced Understand Dataclasses Advanced through an everyday analogy so Python behavior feels intuitive, not random.