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.
- 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
@nameabove a function is a decorator: it wraps the function; the built-in@classmethod,@staticmethodand@dataclassare enough here, in detail — in the lesson on decorators, later on the track.
self, the bound method, class attributes and instance attributes,vars();@classmethod,@staticmethod,super().__init__(),_xand__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.
class Greeter:
def __init__(self, name):
self.name = name
def hello(self):
return f"hello, {self.name}"
a = Greeter("Anna")
print(a.hello()) # hello, AnnaGreeter("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
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 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.hellois 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.helloisFalse, 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 callinga.hello()usually creates no such object at all, is in the article on attribute lookup. - A forgotten
selfbreaks 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
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 "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.namelooks 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. Afterc1.total = 5,c1has its owntotal, 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
Methods differ in what they receive as the first argument:
| method | first argument | used for |
|---|---|---|
| ordinary | the instance (self) | working with the instance's data |
@classmethod | the class (cls) it was called through | alternative constructors, working with class attributes |
@staticmethod | nothing extra | a 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__()
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.
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
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.__xinside a class is renamed (except names that also end in two underscores — those are magic methods such as__init__) — "the identifier__spamoccurring in a class namedFoo,_Fooor__Foois 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
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.
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
__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:
OnlySlotswith__slots__ = ("x",), whose only base class isobject, 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__ = (), whereBaseis an ordinary class without slots. The parent gives the instance a dictionary:c.extra = 1goes through and lands invars(c).SlotsWithDictwith"__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
What is a class attribute good for, if it has so many traps?
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.
What is a property, and what does it have to do with attributes?
@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.
How do you compare two objects by value?
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.
What happens with multiple inheritance?
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
self is a keyword, and Python passes it in some special way
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.
An attribute declared in the class body is copied into each instance
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.
__init__ is the constructor
__new__ creates the object; __init__ receives the object that already exists and fills it in. In the run __new__ prints before __init__.
A double underscore makes an attribute private
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.
Once you define __eq__, the object can go into a set as before
It cannot: __hash__ implicitly becomes None, and {Point(1, 2)} fails with TypeError: unhashable type. It needs its own __hash__ over the same fields.
If a class has __slots__, its instances certainly have no __dict__
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
| Version | Change | What it means for code |
|---|---|---|
| 3.0 | Old-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.11 | The 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.13 | In 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.14 | In 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.15 | Released 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
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
Check yourself
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.
This is neither a retelling nor a separate text: everything below is taken from the article itself — its own summary, the section headings, the “actually” column and the version table. Which is why these theses cannot drift from the article.
The gist
- 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, andselfis its ordinary first argument:a.hello()andGreeter.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.namelooks in the instance first, then in the class, while assigningself.name = …writes to the instance. That is whyc1.total = 5does not change the class, whilec1.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, whiletagsis not in their own dictionaries. - Beyond that comes what separates understanding from having read about it.
a.hellois a new bound method object on every access: a thousand accesses gave a thousand different objects.@classmethodreceives the class it was called through — which is why the factoryDog.create()returns aDog, 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.
In fact
- It is an ordinary first parameter.
a.hello()andGreeter.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. - 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 runc1andc2saw all three tags, whilec2's dictionary held nothing. __new__creates the object;__init__receives the object that already exists and fills it in. In the run__new__prints before__init__.- 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. - It cannot:
__hash__implicitly becomesNone, and{Point(1, 2)}fails withTypeError: unhashable type. It needs its own__hash__over the same fields. - 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__ = ()acceptedc.extra = 1, andvars(c)became{'extra': 1}.
By version
- 3.0
- Old-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.11
- The 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.13
- In 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 attributesinstead 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.14
- In 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')andcannot use 'Point' as a dict key (…)instead ofunhashable type: 'Point'. The behaviour is the same. Tests that compare the exception text in full will break on 3.14.< - 3.15
- Released 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.<
What is covered
- Base: a class is an object with shared attributes
- Mechanism 1: `self` is an ordinary first argument
- Mechanism 2: class attributes and instance attributes
- Mechanism 3: three kinds of methods
- Mechanism 4: inheritance and `super().__init__()`
- Mechanism 5: `_x` and `__x` — a convention, not protection
- Deeper: `__init__` is not a constructor, and `__eq__` switches the hash off
- Deeper: `__slots__` does not always remove the dictionary
- How to answer in an interview
- Next they ask
- Common misconceptions
- Version history
- Practice
- Check yourself
- How it was measured
Sources & further reading
5 SOURCES
- 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
- 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
- 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__
- 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
- 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