skip to content

Why does `class Cls: __slots__ = ('__buf',)` create a slot named `_Cls__buf`?

level: middleimportance: nice to knowfreq 15%

answer

  1. Two machineries have to agree on one name
  2. One is the compiler, one is class creation
  3. Slot strings are the string exception
  4. The declared tuple is left unmodified
  5. dir() and __slots__ disagree

basics

~10 s

Class creation applies the same private-name mangling to __slots__ entries that the compiler applies to identifiers, so the descriptor is built as _Cls__buf — exactly the name self.__buf compiles to inside the class body.

solid answer

~40 s

The strings in `__slots__` are the one place the language mangles a string rather than an identifier. When the class object is created, each slot name that starts with two or more underscores and does not end in two is rewritten with the class-name prefix before the descriptor is installed, so `dir(Cls)` shows `_Cls__buf`. That special case exists so the declaration lines up with the code: `self.__buf` inside `Cls` compiled to `self._Cls__buf`, and without the rewrite it would find no slot. Two details are worth knowing. The `__slots__` tuple itself is not modified — `Cls.__slots__` still reads `('__buf',)`. And a subclass that declares `__slots__ = ('__buf',)` gets its own `_Sub__buf` descriptor rather than shadowing the base's, which is the same collision avoidance mangling provides everywhere else.

code

python · 15 lines
python
class Cls:
    __slots__ = ("__buf",)

    def __init__(self):
        self.__buf = []

    def add(self, item):
        self.__buf.append(item)


c = Cls()
c.add(1)
print([n for n in dir(Cls) if "buf" in n])   # ['_Cls__buf']
print(Cls.__slots__)                          # ('__buf',)
print(c._Cls__buf)                            # [1]

go deeper

for a junior

Just remember the outcome: a slot declared '__buf' inside a class shows up as _Cls__buf, and code written self.__buf inside that class finds it. You do not need the machinery to use it.

for a middle

Explain which side does what — the compiler rewrites the identifier in the method body, class creation rewrites the slot string — and why they must agree or the first assignment would raise AttributeError.

for a senior

Show where it bites: tooling that reads __slots__ and calls getattr fails on exactly these names, because the declared tuple keeps the unmangled spelling. Say what you would do instead in a library meant to be introspected.

for a principal

Treat it as an introspectability decision: double-underscore slots buy subclass collision safety and cost every serializer, diff tool and debugger a class-name-aware prefix step. Decide which your public types owe their consumers.

Private name mangling is normally described as a compile-time rewrite of *identifiers*, and everything that reaches an attribute by *string* — `getattr`, `setattr`, `hasattr` — is untouched by it. `__slots__` is the documented exception that proves the shape of the rule, and it is a satisfying question precisely because it forces you to say why an exception was necessary. ## What actually happens ```python class Cls: __slots__ = ("__buf",) def __init__(self): self.__buf = [] print([n for n in dir(Cls) if "buf" in n]) # ['_Cls__buf'] print(Cls.__slots__) # ('__buf',) ``` Two different pieces of machinery meet here. The **compiler** rewrote `self.__buf` in the method body to `self._Cls__buf`, as it does for any double-underscore identifier in a class body. The **class-creation** machinery, when it turns the class body's namespace into a type object, walks the `__slots__` entries and applies the same private-name transform to each string before creating the slot descriptors. Both sides therefore agree on `_Cls__buf`, and the assignment in `__init__` finds its slot. Had the class-creation side not done this, a slot declared as `"__buf"` would have produced a descriptor literally named `__buf`, while every reference written `self.__buf` in the body looked for `_Cls__buf` — a slotted class with no `__dict__` would then raise `AttributeError` on its own first assignment. The special case exists to make the obvious spelling work. ## The tuple is left alone The rewrite happens on the way to building descriptors; the `__slots__` object stored on the class is the tuple you wrote. So introspection gives you two different spellings depending on where you look: `Cls.__slots__` shows `'__buf'`, while `dir(Cls)`, `Cls.__dict__` and `vars(instance_of_a_subclass_with_a_dict)` show `_Cls__buf`. Tooling that reads `__slots__` to discover an object's fields — a serializer, a `__getstate__` helper, a diffing utility — must mangle the names itself before using them with `getattr`, or it will fail on exactly the attributes the author marked as internal. ## Writing the mangled name by hand also works Because the compiler and the class-creation machinery independently arrive at the same string, declaring `__slots__ = ("_Cls__buf",)` and writing `self.__buf` in the body works too: the declared name already matches what the identifier compiles to, and the mangling rule leaves a name starting with a single underscore alone. Nobody should write that on purpose, but recognising why it works is a good check that you understand which side does what. ## Subclasses Mangling gives slots the same collision safety it gives ordinary attributes. A base declaring `__slots__ = ("__buf",)` and a subclass declaring the same tuple end up with two independent descriptors, `_Base__buf` and `_Sub__buf`, and each class's methods use its own. Without the rewrite the subclass declaration would redundantly redeclare the base's slot — a documented waste of space, since the base's descriptor remains accessible and the duplicate simply hides it. ## Where the line is The memory model of `__slots__` — no per-instance `__dict__`, descriptors on the class, what happens when a base lacks slots — is a separate subject. What matters here is narrow and precise: `__slots__` entries are mangled, strings passed to `getattr` and `setattr` are not, and the reason for the difference is that the slot declaration must agree with code the compiler already rewrote. ## Reaching a slotted mangled attribute from outside Because the descriptor lives on the class under the mangled name, external access is the mangled spelling, `instance._Cls__buf`, exactly as for a `__dict__`-backed attribute. Deleting it with `del instance._Cls__buf` empties the slot and a subsequent read raises `AttributeError` — the ordinary slot behaviour, reached through the ordinary mangled name. Nothing about mangling changes the descriptor semantics; it only decides the string the descriptor is filed under. ## The interview point in one line If you are asked this, the answer that lands is: mangling normally applies to identifiers the compiler sees, `__slots__` entries are the exception because the declaration is a string that has to agree with identifiers the compiler already rewrote, and the rewrite happens on the way to the descriptor rather than to the tuple you wrote. Everything else about `__slots__` — instance layout, the missing per-instance dictionary, inheritance rules — is a separate subject.

  • A serializer reads `__slots__` and calls `getattr` for each entry. What goes wrong?
    It fails on every double-underscore entry. `Cls.__slots__` reports the unmangled string `'__buf'`, but the attribute actually lives under `_Cls__buf`, and `getattr` does not rewrite strings. Such a helper has to apply the prefix itself for each defining class it walks — or the class should avoid double-underscore slot names in the first place.
  • What happens if a base and a subclass both declare `__slots__ = ('__buf',)`?
    Each gets its own descriptor, `_Base__buf` and `_Sub__buf`, and neither hides the other — the same collision avoidance mangling provides for ordinary attributes. Repeating an *unmangled* slot name in a subclass, by contrast, creates a second descriptor that shadows the base's and wastes the space the base already reserved.

saying these in an interview costs you the question

  • Says `__slots__` strings are used exactly as written
  • Expects `Cls.__slots__` to show the mangled spelling
  • Claims strings are mangled generally, including for getattr
  • Thinks a subclass repeating `__slots__ = ('__buf',)` overrides the base slot
  • Believes the compiler rewrites the tuple literal itself

context