skip to content

Why does `getattr(obj, "__cache")` fail when `self.__cache` works inside the class?

level: seniorimportance: should knowfreq 38%

answer

  1. Where exactly does the rewrite happen?
  2. Source identifiers versus runtime strings
  3. The compiler never sees your string
  4. setattr silently creates a second key
  5. vars() shows the _ClassName__ prefix

basics

~20 s

Mangling rewrites identifiers in source code, not strings. self.__cache in the class body compiled to self._Worker__cache, while the string handed to getattr is used verbatim, so the lookup asks for a literal __cache that does not exist.

solid answer

~40 s

Private name mangling is a compile-time transform over identifiers. A string is data: `getattr`, `setattr`, `hasattr` and `delattr` never rewrite it. So inside `class ThumbnailWorker`, `self.__cache = {}` created the key `_ThumbnailWorker__cache`, and `getattr(worker, "__cache")` raises `AttributeError` because no attribute of that literal name exists. The more dangerous half is the write side. `setattr(worker, "__cache", value)` does not fail — it quietly adds a second, unrelated `__cache` entry to the instance `__dict__` that the class's own code will never read. Anything reflective sees the same split: `vars(obj)` and `dir(obj)` show the mangled spelling, and a `__getattr__` fallback is handed the mangled name. If reflective code must reach an attribute, either spell the mangled name explicitly or, better, do not use a double underscore for it.

code

python · 15 lines
python
class ThumbnailWorker:
    def __init__(self):
        self.__cache = {}

    def store(self, key, value):
        self.__cache[key] = value


w = ThumbnailWorker()
w.store("a", 1)
print(vars(w))
print(hasattr(w, "__cache"), hasattr(w, "_ThumbnailWorker__cache"))
setattr(w, "__cache", "unrelated")
w.store("b", 2)
print(vars(w))

go deeper

for a junior

Remember the one-line rule: the compiler rewrites names you type, never strings you pass. If getattr with a double-underscore name fails, print vars(obj) and you will see the real key.

for a middle

Explain both directions — the read that raises and the write that silently creates a duplicate key — and name the reflective surfaces that show the mangled spelling: vars, dir, hasattr and __getattr__.

for a senior

Diagnose it in real code: an injected fake that never takes effect, a serializer whose payload embeds the class name, a config-driven attribute setter that misses. Then give the fix — single-underscore names for anything reflection must reach.

for a principal

Own the convention across a codebase: mangled attributes and reflective machinery — serializers, fixtures, config binding, plugin loaders — are a standing incompatibility, and the guidance for library authors should say which side gives way.

This question separates people who have *read* that double underscores are mangled from people who know **where** the mangling happens. The answer is: in the compiler, over identifiers in source text. Everything that reaches an attribute through a *string* at run time bypasses it entirely. ## The concrete split Consider an image-thumbnail worker that keeps a rendered-thumbnail cache running at an 83% hit rate: ```python class ThumbnailWorker: def __init__(self): self.__cache = {} def store(self, key, value): self.__cache[key] = value ``` Inside the class body the compiler rewrote both occurrences to `self._ThumbnailWorker__cache`. That is the key in the instance dictionary. Now a generic stats helper walks a list of attribute names and calls `getattr(worker, name)`; for `"__cache"` it raises `AttributeError: 'ThumbnailWorker' object has no attribute '__cache'`. Nothing rewrote the string, so nothing matched. ## The write side is worse than the read side A failed read is loud. A write is silent: ```python setattr(worker, "__cache", "unrelated") vars(worker) # {'_ThumbnailWorker__cache': {...}, '__cache': 'unrelated'} ``` The instance now carries two attributes whose names differ only by the prefix, and the class's own methods will keep using the mangled one forever. Configuration loaders, test helpers that "inject" a fake, deserializers rebuilding an object from a dict, and fixtures that patch state by string name all hit this. The object looks patched and behaves as though it was not. ## Everything reflective sees the mangled spelling * `vars(obj)` and `obj.__dict__` are keyed by the mangled name. * `dir(obj)` lists the mangled name. * `hasattr(obj, "__cache")` is `False`; `hasattr(obj, "_ThumbnailWorker__cache")` is `True`. * A `__getattr__` fallback defined on the class is called with the **mangled** name, because the failed lookup was for the mangled attribute — a `__getattr__` that dispatches on the string it receives must expect `_ThumbnailWorker__missing`, not `__missing`. * `copy`, `pickle` and any `__dict__`-walking serializer round-trip the mangled key, which means the class name is now embedded in your serialized data. Rename the class and old payloads no longer load. ## The one string that *is* mangled There is a single well-known exception, and knowing it demonstrates that you understand the boundary rather than a slogan: the entries of `__slots__` are mangled by the class-creation machinery when the slot descriptors are built. That is a deliberate special case so that `self.__buf` inside the body matches the slot declared as `"__buf"`. It is not a general rule about strings — no other string in the language is treated that way. ## How to work with it First, if reflective code must reach an attribute, do not name it with two leading underscores. A single underscore already says "internal" and stays greppable, patchable and serializable. Second, if you must reach one you do not own, build the name rather than hard-coding it — `f"_{type(obj).__name__}__cache"` is fine for a debug script, but note that it is wrong the moment the attribute was defined in a base class rather than in the object's own class, since the prefix is the *defining* class's name. Third, when reviewing framework-shaped code — anything that resolves attribute names from configuration, from a schema or from a test fixture — treat every double-underscore attribute in the target classes as a trap and say so. ## Diagnosing it in the wild The symptom is usually "the attribute exists but reflection cannot find it", or "I set it and nothing changed". One line settles both: print `vars(obj)`. A `_ClassName__` prefixed key next to a bare double-underscore key is the whole diagnosis, and the class name in the prefix tells you which class body performed the rewrite. ## The same trap through the standard library Anything in the stdlib that takes an attribute *name as a string* inherits the problem: ```python import operator operator.attrgetter("__cache")(worker) # AttributeError: 'ThumbnailWorker' object has no attribute '__cache' ``` `unittest.mock.patch.object(worker, "__cache", {})` fails the same way — it resolves its target with `getattr`, so it raises `AttributeError: ... does not have the attribute '__cache'` before any patching happens, and you have to pass `"_ThumbnailWorker__cache"` for the patch to take. `patch("pkg.mod.Cls.__cache")` in string form has exactly the same requirement. When a test cannot patch an attribute that plainly exists on the object, the mangled name is the first thing to check, and it is a good argument for giving anything a test needs to replace a single-underscore name.

  • How do you reach a mangled attribute from a debugging script when you must?
    Spell the mangled name: `getattr(obj, "_ThumbnailWorker__cache")` or `vars(obj)["_ThumbnailWorker__cache"]`. Building it from `type(obj).__name__` works only when the attribute was assigned in that exact class — if a base class assigned it, the prefix is the base's name, not the runtime type's. Keep this in throwaway tooling, never in shipped code.
  • What breaks when a class with mangled attributes is pickled and then renamed?
    The pickled `__dict__` carries keys like `_ThumbnailWorker__cache`, so the class name is embedded in the payload. Renaming the class means new code writes and reads `_NewName__cache` while old payloads still hold the old key, and the restored object silently has no cache. Single-underscore names avoid the coupling entirely.
  • Which name does a class's `__getattr__` receive for a failed `self.__missing` lookup?
    The mangled one — `_Probe__missing` for `class Probe`. The rewrite happened before the lookup was ever attempted, so the fallback hook sees exactly what the attribute machinery searched for. Any `__getattr__` that routes on the string, such as a proxy or a lazy-attribute loader, must account for that prefix.

The compiler renames the door before the building opens. Anyone who memorised the old label and asks for it by name at run time is asking about a door that no longer exists.

saying these in an interview costs you the question

  • Expects getattr to apply the same rewrite as the compiler
  • Thinks setattr with the literal name updates the mangled attribute
  • Assumes `__getattr__` is handed the unmangled name
  • Says the attribute is private so reflection is blocked
  • Hardcodes a prefix from the runtime type when a base class defined the attribute

context