Deep Engineering
Intermediate·Published·3.11 · 3.12 · 3.13 · 3.14·30 MIN

Classes and OOP: self is an argument, and a class attribute is shared by all

The first OOP questions sound simple: what self is, how a class attribute differs from an instance attribute, why classmethod and staticmethod exist, what __init__ does. A run answers each one: a.hello() and Greeter.hello(a) are the same call; a list declared in the class body is one for all instances; __init__ receives an object that already exists; and a class with __eq__ but no __hash__ can no longer be a dictionary key or a set element.

Full technical treatment

TL;DR

A class in Python is an object holding the attributes and methods shared by all its instances; an instance of an ordinary class is an object with its own dictionary of attributes. (A class with __slots__ can do without an instance dictionary, but only under the conditions covered at the end of the lesson.) A method is an ordinary function in the class body, and self is its ordinary first argument: a.hello() and Greeter.hello(a) are the same call. Python simply puts the instance in as the first argument itself.

Hence the main consequence: for an ordinary attribute, reading self.name looks in the instance first, then in the class, while assigning self.name = … writes to the instance. That is why c1.total = 5 does not change the class, while c1.tags.append(…) does: it is a read of the shared list declared in the class body. Three instances that each added a tag to such a list all see all three tags, while tags is not in their own dictionaries.

Beyond that comes what separates understanding from having read about it. a.hello is a new bound method object on every access: a thousand accesses gave a thousand different objects. @classmethod receives the class it was called through — which is why the factory Dog.create() returns a Dog, not the parent. __init__ is not a constructor: by the time it runs, __new__ has already created the object. And a class that defined __eq__ and forgot __hash__ can no longer be a dictionary key or a set element: TypeError: unhashable type.

Where to start
Before this lesson it is enough to understand
  • how to declare a function and call it with arguments;
  • what a list and a dictionary are, and that a list can be changed in place;
  • that a class declaration is code that runs from top to bottom;
  • what a set (set) is, and that a dictionary key and a set element must be hashable — that is, have a hash, the number a dictionary uses to place its keys; in detail — in the lesson on built-in collections, the next one on the track;
  • that a line @name above a function is a decorator: it wraps the function; the built-in @classmethod, @staticmethod and @dataclass are enough here, in detail — in the lesson on decorators, later on the track.
You do not need to know in advance
  • self, the bound method, class attributes and instance attributes, vars();
  • @classmethod, @staticmethod, super().__init__(), _x and __x;
  • __new__, __init__, __eq__, __hash__ and __slots__.

What it was checked on: all output in the lesson comes from runs of bench/oop/basics.py and bench/oop/practice.py on CPython 3.11.15, 3.12.3, 3.13.7 and 3.14.7, linux/amd64; the blocks in the text are the 3.13.7 record, with the script's labels and Russian strings translated; differences between versions are in the "Version history". The documentation quotes were checked on 14.10.2026. By that date Python 3.15 was out (9 October 2026); the lesson was not run on it.

Base: a class is an object with shared attributes

A class is declared like this. Full file — no imports needed; run it with python3 greeter.py.

PYTHON
class Greeter:
    def __init__(self, name):
        self.name = name
 
    def hello(self):
        return f"hello, {self.name}"
 
a = Greeter("Anna")
print(a.hello())        # hello, Anna

Greeter("Anna") creates an instance and calls its __init__, which puts a name attribute into the instance. The hello method lives in the class, one for all instances, while the data — name — is each instance's own.

A junior-level answer to "what is a class": a class describes data and behaviour; the data lives in the instances, the behaviour — the methods — in the class, shared by all. self is a reference to the instance the method is working with. That is correct, but it is immediately followed by the questions of where self comes from and what happens if you put data in the class rather than in the instance. That is what the mechanisms below are about.

Inheritance, encapsulation and polymorphism exist in Python too, but they are simpler than in languages where they are taught first: a subclass looks up an attribute in its parent, "privacy" is a naming convention, and polymorphism is just calling a method of the same name on an object of another class.

Mechanism 1: self is an ordinary first argument

language contractThe link between a method and a function is set by the data model; the output is a run of bench/oop/basics.py, section 1, identical on CPython 3.11–3.14.

self is not a keyword and not magic. The documentation describes a method call like this:

When an instance method object is called, the underlying function (__func__) is called, inserting the class instance (__self__) in front of the argument list.

— The data model — instance methods

The run shows it directly — output of bench/oop/basics.py, section 1; labels and the script's Russian strings translated:

1) self is an ordinary first argument
   a.hello()          -> hello, Anna
   Greeter.hello(a)   -> hello, Anna
   type(a.hello)      -> method
   a.hello is a.hello -> False
   a.hello == a.hello -> True
   a.hello.__self__ is a            -> True
   a.hello.__func__ is Greeter.hello -> True
   Greeter.hello()    -> TypeError: Greeter.hello() missing 1 required positional argument: 'self'
   a.broken()         -> TypeError: Greeter.broken() takes 0 positional arguments but 1 was given
   1000 accesses to a.hello, distinct objects: 1000

Three things are worth taking from this.

  • a.hello is a bound method: a small object that remembers the function (__func__) and the instance (__self__). Calling a bound method calls the function with the instance put in as the first argument.
  • It is created anew on every access: a.hello is a.hello is False, although they are equal. How exactly a function in a class turns into a bound method is covered in the lesson on descriptors; what it costs, and why calling a.hello() usually creates no such object at all, is in the article on attribute lookup.
  • A forgotten self breaks on the first call through an instance, and the error message says exactly that: a function declared with no parameters was given one argument — the instance.

Mechanism 2: class attributes and instance attributes

language contractThe lookup and write rule is the language reference (class definitions); the output is a run of bench/oop/basics.py, section 2, identical on CPython 3.11–3.14.

The documentation states the rule in one paragraph:

Variables defined in the class definition are class attributes; they are shared by instances. […] Class attributes can be used as defaults for instance attributes, but using mutable values there can lead to unexpected results.

— The language reference — class definitions

The "unexpected results" look like this — output of bench/oop/basics.py, section 2; labels translated:

2) class attribute versus instance attribute
   before assignment: vars(c1) = {} | c1.total = 0
   after c1.total = 5: vars(c1) = {'total': 5}
   c1.total = 5 | c2.total = 0 | Counter.total = 0
   three instances each added a tag to the class's tags:
   c1.tags = ['t0', 't1', 't2'] | c2.tags = ['t0', 't1', 't2']
   c1.tags is c2.tags -> True | vars(c2) = {}
   list in __init__: f1.tags = ['t0'] | f2.tags = []

All of it follows from one rule with two halves.

  • Reading c1.name looks in the instance dictionary (vars(c1)) first, then in the class. As long as an instance has no attribute of its own, all instances read the same class value.
  • Assignment c1.name = … writes to the instance dictionary. After c1.total = 5, c1 has its own total, which shadows the class one; the class and the other instances are unchanged.

The rule holds for ordinary attributes — and they are the vast majority in code. There is one exception, and it is the next question asked: if the class holds a data descriptor under that name — a property, for example — then both reading and writing go through it rather than through the instance dictionary. This is covered in the lesson on descriptors. __slots__ stands apart: the documentation implements its names with those same descriptors, and it can remove the instance dictionary — but not always; the conditions are in the section "Deeper: __slots__ does not always remove the dictionary" below.

The trap is that c1.tags.append(…) is not an assignment. It is a read of c1.tags, which finds the list in the class, and a method call on that list. There is one list, so it changes for everyone. The fix is to create the mutable value in __init__: self.tags = [] — then each instance has its own. The same mechanism — a value computed once and shared by everyone — is behind the mutable default argument of functions, and the lesson on it covers how dataclasses catch this mistake.

Mechanism 3: three kinds of methods

language contractWhat a method receives as its first argument is defined by the data model; the output is a run of bench/oop/basics.py, section 3, identical on CPython 3.11–3.14.

Methods differ in what they receive as the first argument:

methodfirst argumentused for
ordinarythe instance (self)working with the instance's data
@classmethodthe class (cls) it was called throughalternative constructors, working with class attributes
@staticmethodnothing extraa function that is convenient to keep in the class namespace

Output of bench/oop/basics.py, section 3; labels translated:

3) three kinds of methods: what comes as the first argument
   describe got an instance of Dog
   who got the class Dog | who got the class Dog
   helper got only x = 1 | helper got only x = 2
   Animal.create() -> Animal
   Dog.create()    -> Dog
   Animal.describe() without an instance -> TypeError: Animal.describe() missing 1 required positional argument: 'self'
   MyDict.fromkeys('ab')         -> MyDict
   MyDateTime.fromtimestamp(0)   -> MyDateTime

The key lines here are the ones about create. The create method is declared in Animal and does return cls(). Called through Dog, it receives cls = Dog and creates a dog, even though it is written in the parent. Standard library factories work the same way: in the last two lines dict.fromkeys and datetime.fromtimestamp, called through subclasses, returned the subclasses. If a factory wrote Animal() explicitly, a subclass would get the parent's object out of it.

@staticmethod receives neither the instance nor the class. It can be called through the class or through an instance — it behaves the same way.

Mechanism 4: inheritance and super().__init__()

language contractThe requirement to call the base class __init__ is the data model; the output is a run of bench/oop/basics.py, section 4, identical on CPython 3.11–3.14.

A subclass looks up in its parent everything it does not have itself. But if the subclass declares its own __init__, the parent's is not called by itself:

If a base class has an __init__() method, the derived class's __init__() method, if any, must explicitly call it to ensure proper initialization of the base class part of the instance.

— The data model — __init__

Output of bench/oop/basics.py, section 4; labels and the script's Russian strings translated:

4) inheritance and super().__init__()
   without super(): the object was created, vars = {'rate': 0.05}
   s1.balance -> AttributeError: 'SavingsForgot' object has no attribute 'balance'
   with super():    vars = {'owner': 'Ira', 'balance': 0, 'rate': 0.05}
   isinstance(s2, Account) -> True
   type(s2) is Account     -> False
   Savings.__mro__ -> ['Savings', 'Account', 'object']

Without super().__init__() the object is created with no error, and the AttributeError appears later — on the first read of the parent's attribute, far from the place where the call was forgotten. The lines about isinstance and type(s2) is Account show the difference between a membership check and comparing types: an instance of the subclass is an instance of the parent, but its type is not the parent. That is why the check is done with isinstance when subclasses should pass it.

The order in which Python looks an attribute up along the chain of classes lives in __mro__. It is built by the C3 algorithm; with single inheritance the result is obvious — the class, its parent, object — but with multiple inheritance it is not: that is the lesson on the MRO.

Mechanism 5: _x and __x — a convention, not protection

language contractThe name mangling rule is the language reference; the output is a run of bench/oop/basics.py, section 5, identical on CPython 3.11–3.14.

Python has no access modifiers. There are two naming conventions:

  • _x — "this is internal, don't touch it from outside". The interpreter does nothing about it: the attribute is accessible like any other.
  • __x inside a class is renamed (except names that also end in two underscores — those are magic methods such as __init__) — "the identifier __spam occurring in a class named Foo, _Foo or __Foo is transformed to _Foo__spam".

Output of bench/oop/basics.py, section 5; labels translated:

5) _x and __x: a convention and renaming
   vars(w) = {'_cash': 10, '_Wallet__pin': 1234}
   w._cash -> 10
   w.__pin -> AttributeError: 'Wallet' object has no attribute '__pin'
   w._Wallet__pin -> 1234
   a subclass with the same __pin: vars = {'_cash': 10, '_Wallet__pin': 1234, '_StrictWallet__pin': 0}
   sw.pin_ok(1234) -> True

The renaming does not hide the attribute — w._Wallet__pin reads freely. It protects against something else: an accidental name clash in a subclass. The subclass declared its own __pin, and it landed next to the parent's rather than on top of it: the parent's pin_ok method still sees its own _Wallet__pin. That is the answer to "does Python have private": no, it has protection against clashes.

Deeper: __init__ is not a constructor, and __eq__ switches the hash off

language contractThe order of __new__ and __init__ and the __hash__ = None rule are the data model; the output is a run of bench/oop/basics.py, section 6. The exception text changed in 3.14 — the versions are in the history below.

Two magic methods that interview answers most often get wrong.

__init__ receives an object that already exists. The documentation says it is "called after the instance has been created (by __new__()), but before it is returned to the caller". A constructor in the usual sense is a pair: __new__ creates the object, __init__ fills it in. __new__ is overridden rarely: the documentation names two cases — subclasses of immutable types such as int, str and tuple, and metaclasses.

__eq__ without __hash__ makes the object unhashable.

A class that overrides __eq__() and does not define __hash__() will have its __hash__() implicitly set to None.

— The data model — __hash__

Output of bench/oop/basics.py, section 6; labels translated:

6) magic methods: __new__, __init__, __eq__, __hash__
   __new__ creates the object
   __init__ fills in the object that already exists, x = 1
   Plain(1, 2) == Plain(1, 2) -> False
   Point(1, 2) == Point(1, 2) -> True
   Point.__hash__ is None     -> True
   {Point(1, 2)}              -> TypeError: unhashable type: 'Point'
   {Point(1, 2): 'x'}         -> TypeError: unhashable type: 'Point'
   len({HPoint(1, 2), HPoint(1, 2)}) -> 1
   repr(Plain(1, 2)) starts with -> <__main__.Plain object
   @dataclass: DPoint.__hash__ is None -> True
   @dataclass(frozen=True): len({FPoint(1, 2), FPoint(1, 2)}) -> 1

The logic of the rule is simple. The default hash is computed from the object's identity — id(), which in CPython is the address — and dictionaries and sets require equal objects to have equal hashes. Once equality became "by value", a hash "by address" would contradict it — so Python switches it off instead of silently breaking the dictionary. Without __eq__, objects are compared by identity: Plain(1, 2) == Plain(1, 2) is False. If the object needs to be a key, __hash__ is written over the same fields as __eq__ — then a set of two equal points holds one. And those fields must not change while the object sits in a dictionary.

On 3.14 the error message is different and names the object's role — cannot use 'Point' as a set element (unhashable type: 'Point') and cannot use 'Point' as a dict key (unhashable type: 'Point'); the meaning is the same.

Deeper: __slots__ does not always remove the dictionary

language contractWhat __slots__ does and when the dictionary is there anyway is the data model (the __slots__ section); the output is a run of bench/oop/basics.py, section 7, added on 14.10.2026. The behaviour is the same on CPython 3.11–3.14; the AttributeError text is longer on 3.13 and 3.14 — the versions are in the history below.

__slots__ is a class attribute listing the names the instances will have. The common retelling — "a class with __slots__ has no instance dictionary" — holds only under conditions. The documentation is more careful: slots "reserves space for the declared variables and prevents the automatic creation of __dict__ and __weakref__ for each instance", and right away it adds: "When inheriting from a class without __slots__, the __dict__ and __weakref__ attribute of the instances will always be accessible". The second way to a dictionary is to list "__dict__" in the slots themselves.

Output of bench/oop/basics.py, section 7; the label line translated:

7) __slots__: when an instance has no __dict__, and when it has one
   OnlySlots: hasattr(o, '__dict__') -> False
   OnlySlots: o.extra = 1 -> AttributeError: 'OnlySlots' object has no attribute 'extra' and no __dict__ for setting new attributes
   Child(Base), __slots__ = (): c.extra = 1 -> vars(c) = {'extra': 1}
   Child(Base): hasattr(c, '__dict__') -> True
   __slots__ = ('x', '__dict__'): s.extra = 1 -> vars(s) = {'extra': 1}

The three classes from the run:

  • OnlySlots with __slots__ = ("x",), whose only base class is object, and no "__dict__" in the slots. Here the instance really has no dictionary, and a name not listed in the slots cannot be assigned.
  • Child(Base) with __slots__ = (), where Base is an ordinary class without slots. The parent gives the instance a dictionary: c.extra = 1 goes through and lands in vars(c).
  • SlotsWithDict with "__dict__" in its slots: the dictionary is switched on explicitly.

Hence the precise wording: for a class with __slots__ where no base class provides an instance dictionary and "__dict__" is not in the slots, the instances get no __dict__. As for the slot names themselves, the documentation says they "are implemented at the class level by creating descriptors for each variable name" — this is exactly the descriptor caveat of mechanism 2; in detail — in the lesson on descriptors.

How to answer in an interview

Short answer: a class holds the attributes and methods shared by its instances, an instance of an ordinary class holds its own data in its own dictionary. A method is a function in the class, self is its first argument: a.m() is equivalent to A.m(a). Reading an ordinary attribute looks in the instance, then in the class; assignment through an instance writes to the instance. @classmethod receives the class, @staticmethod receives nothing.

That is enough for a correct answer. What follows is what you add if the interviewer digs.

If the interviewer digs deeper

On "what is self". "An ordinary first parameter. a.hello is a bound method: an object that remembers the function and the instance and puts the instance in as the first argument when called. In my run a.hello() and Greeter.hello(a) gave the same result, while a.hello is a.hello was False: the object is new every time."

On "class attribute versus instance attribute" — the trap. "A list declared in the class body is one for all instances: three instances each added a tag — each sees three. c1.tags.append is a read, not an assignment. Mutable state is created in __init__."

On "why classmethod". "It receives the class it was called through. A create factory with return cls(), declared in the parent, creates the subclass when called through the subclass — in my run Dog.create() returned a Dog."

On "does Python have private". "No. _x is a convention. __x is renamed to _Class__x, and that protects against a name clash in a subclass, not against access."

On "is __init__ a constructor?". "No, the object has already been created by __new__ at that point. And if you override __eq__ and the object has to be a dictionary key, you must define __hash__ too: otherwise it becomes None."

On "does a class with __slots__ have no __dict__?". "Only if no base class provides a dictionary and "__dict__" is not in the slots. A subclass of an ordinary class with __slots__ = () gets the dictionary from its parent — in my run c.extra = 1 went through, and vars(c) became {'extra': 1}."

Next they ask

Next they ask

What is a class attribute good for, if it has so many traps?

Short answer

An immutable class attribute is normal practice: a constant shared by all instances (sound = "woof"), or a default that an instance can override by assignment. Only mutable ones are a trap: they are changed in place rather than assigned, and the change reaches everyone.

Next they ask

What is a property, and what does it have to do with attributes?

Short answer

@property turns a method into an attribute that is read without parentheses but computed by code: this is how a value is validated on write or computed on the fly without changing the class's interface. It works through descriptors — the same mechanism that turns a function into a bound method. It is covered in the lesson on descriptors.

Next they ask

How do you compare two objects by value?

Short answer

Define __eq__ — and, if the object is needed in a dictionary or a set, __hash__ over the same fields. For "record" classes it is simpler to use @dataclass: it generates __init__, __repr__ and __eq__. Its hash is switched off by default by exactly the rule above — in the run DPoint.__hash__ is None — while with frozen=True the dataclass generates __hash__, and a set of two equal points holds one.

Next they ask

What happens with multiple inheritance?

Short answer

An attribute is looked up along the list of classes __mro__, built by the C3 algorithm, and super() leads not to the parent but to the next class in that list. That is a separate lesson — on the MRO.

Common misconceptions

Claim

self is a keyword, and Python passes it in some special way

Actually

It is an ordinary first parameter. a.hello() and Greeter.hello(a) are the same call; the bound method only puts the instance in as the first argument. You can name the parameter differently — it is just not done.

Claim

An attribute declared in the class body is copied into each instance

Actually

It is one for all and lives in the class. While an instance only reads it, there is no visible difference; a mutable attribute changed in place (append) through any instance changes for all — in the run c1 and c2 saw all three tags, while c2's dictionary held nothing.

Claim

__init__ is the constructor

Actually

__new__ creates the object; __init__ receives the object that already exists and fills it in. In the run __new__ prints before __init__.

Claim

A double underscore makes an attribute private

Actually

It renames the attribute to _Class__name, and under that name it reads freely. The point of the renaming is that a subclass attribute of the same name does not overwrite the parent's.

Claim

Once you define __eq__, the object can go into a set as before

Actually

It cannot: __hash__ implicitly becomes None, and {Point(1, 2)} fails with TypeError: unhashable type. It needs its own __hash__ over the same fields.

Claim

If a class has __slots__, its instances certainly have no __dict__

Actually

Only if no base class provides an instance dictionary and "__dict__" is not in the slots. In the run a subclass of an ordinary class with __slots__ = () accepted c.extra = 1, and vars(c) became {'extra': 1}.

Version history

VersionChangeWhat it means for code
3.0Old-style classes are gone: every class inherits from object, and everything covered in this lesson holds for any class. super() without arguments appears (PEP 3135).
3.11The lesson's reference point. Sections 1–6 of the observation script match line for line on 3.11, 3.12 and 3.13. The runs are CPython 3.11.15, 3.12.3, 3.13.7, 3.14.7 on linux/amd64.
3.13In section 7 (__slots__) the exception text for writing a name not listed in the slots gets longer: 'OnlySlots' object has no attribute 'extra' and no __dict__ for setting new attributes instead of 'OnlySlots' object has no attribute 'extra' on 3.11 and 3.12. The behaviour is the same; on 3.14 the text is as on 3.13.
3.14In the output of the observation script only the exception text for an unhashable object changes: it names the role — cannot use 'Point' as a set element (unhashable type: 'Point') and cannot use 'Point' as a dict key (…) instead of unhashable type: 'Point'. The behaviour is the same. Tests that compare the exception text in full will break on 3.14.
3.15Released on 9 October 2026. The lesson was not run on it: the checked boundary is CPython 3.14.7, as of 14.10.2026. The documentation rules the lesson relies on are quoted from the 3.13 and 3.14 documentation; output on 3.15 is for you to measure.

Practice

Two problems. Answer first, then check against the real output: in both, the correct answer is taken from a run of the script rather than assigned.

Practice · predict the output

A list is declared in the class body, and the add method appends to self.items. Two carts, one item added to each. What will this code print?
class Cart:
  items = []

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


a = Cart()
b = Cart()
a.add("apple")
b.add("pear")
print(len(a.items), len(b.items), a.items is b.items)

Practice · estimate

An object u, whose class has an ordinary method greet. u.greet was put into a list a thousand times in a row. How many distinct objects are there among them?
objects

Check yourself

Question 1 of 4

How does the call a.hello() differ from Greeter.hello(a)?

How it was measured

The findings of this lesson come from these scripts. Each one opens right from here — together with the record of its run: what it ran on and what came out.

Sources & further reading

5 SOURCES

  1. The language reference — class definitionsOfficial documentation. The programmer's note that says everything essential about attributes: «Variables defined in the class definition are class attributes; they are shared by instances. Instance attributes can be set in a method with self.name = value. Both class and instance attributes are accessible through the notation self.name, and an instance attribute hides a class attribute with the same name when accessed in this way. Class attributes can be used as defaults for instance attributes, but using mutable values there can lead to unexpected results». The text was checked against the built-in pydoc help (the pydoc_data.topics module) of CPython 3.13.7 and 3.14.7.https://docs.python.org/3.13/reference/compound_stmts.html#class-definitions
  2. The data model — instance methodsOfficial documentation. What self really is: «When an instance method object is called, the underlying function (__func__) is called, inserting the class instance (__self__) in front of the argument list». And further: «calling x.f(1) is equivalent to calling C.f(x, 1)». Checked against pydoc_data.topics of 3.13.7 and 3.14.7.https://docs.python.org/3.13/reference/datamodel.html#instance-methods
  3. The data model — __init__ and __hash__Official documentation. When __init__ is called: «Called after the instance has been created (by __new__()), but before it is returned to the caller». On inheritance: «If a base class has an __init__() method, the derived class's __init__() method, if any, must explicitly call it to ensure proper initialization of the base class part of the instance». And on the hash: «A class that overrides __eq__() and does not define __hash__() will have its __hash__() implicitly set to None». Why __new__ is overridden at all: «__new__() is intended mainly to allow subclasses of immutable types (like int, str, or tuple) to customize instance creation. It is also commonly overridden in custom metaclasses in order to customize class creation». Checked against pydoc_data.topics of 3.13.7 and 3.14.7.https://docs.python.org/3.13/reference/datamodel.html#object.__init__
  4. The language reference — private name manglingOfficial documentation. Which names count as private: «When an identifier that textually occurs in a class definition begins with two or more underscore characters and does not end in two or more underscores, it is considered a private name of that class». What happens to them: «the identifier __spam occurring in a class named Foo, _Foo or __Foo is transformed to _Foo__spam». Checked against pydoc_data.topics of 3.13.7 and 3.14.7.https://docs.python.org/3.13/reference/expressions.html#private-name-mangling
  5. The data model — __slots__Official documentation. What __slots__ does: «__slots__ reserves space for the declared variables and prevents the automatic creation of __dict__ and __weakref__ for each instance». When the dictionary is there anyway: «When inheriting from a class without __slots__, the __dict__ and __weakref__ attribute of the instances will always be accessible» and «If dynamic assignment of new variables is desired, then add '__dict__' to the sequence of strings in the __slots__ declaration». How they are built: «__slots__ are implemented at the class level by creating descriptors for each variable name». Checked against pydoc_data.topics of 3.13.7 and 3.14.7 on 14.10.2026.https://docs.python.org/3.14/reference/datamodel.html#slots