__slots__ is a class attribute that lists the names an instance is allowed to have. A class that declares it gets instances with no __dict__ (as long as no base class supplies one), and assigning any name that isn’t on the list raises AttributeError. On an ordinary class, a common way to give an attribute a default is to write it in the class body, and on a slotted class that looks like this:

class Point:
    __slots__ = ('x', 'y')
    x = 0
ValueError: 'x' in __slots__ conflicts with class variable

The class statement itself fails, before any instance exists.

Next, take the x = 0 line out and print Point.x. It gives <member 'x' of 'Point' objects>, and 'x' in vars(Point) is True. That object is in the class namespace under the name x, and Python put it there while building the class. The data model reference says __slots__ “are implemented at the class level by creating descriptors for each variable name.” When an instance reads or writes p.x, the lookup finds this member descriptor on the class first, and the descriptor goes to the instance for the value. The instance itself keeps “a fixed-length array of slot values” in place of a dictionary (Descriptor Guide), and the descriptor on the class is the only route from the name x to that value.

A default written as x = 0 would need that same name in the same class namespace. CPython checks for this while it creates the class (in Objects/typeobject.c), and if a slot name is already a class variable it stops with the ValueError above.

Once the class already exists, though, nothing stops you from assigning the class attribute yourself:

class Later:
    __slots__ = ('x',)

Later.x = 0
o = Later()
print(o.x)
o.x = 5
0
AttributeError: 'Later' object attribute 'x' is read-only

This time there’s no error at the assignment. Later.x = 0 simply replaces the descriptor, which is the case the data model reference has in mind when it says the class attribute “would overwrite the descriptor assignment,” and vars(Later)['x'] is now the plain 0. Reading o.x goes to the class and finds that 0 there, the same 0 for every instance, and it looks like a default that worked. Writing o.x = 5 is where it breaks, because the plain 0 has no way to store a value into the instance, and the instance has no __dict__ to fall back to. CPython reports that case as “read-only,” which can be confusing the first time. Nothing in the class says read-only anywhere.

For a default, the simplest place is the __init__ signature, def __init__(self, x=0, y=0), and then self.x = x in the body stores the value into the slot like any other assignment. If the class is a dataclass anyway, the other way is @dataclass(slots=True). There you write x: int = 0 as usual, and the decorator handles the conflict for you. In Lib/dataclasses.py, the code for slots=True takes the field names out of a copy of the class dictionary and builds a new class from it, since __slots__ can’t be added to a class after it exists. The names on the new class are member descriptors again, the same as in a slotted class you write by hand, and the defaults live in the generated __init__.

The examples here were run with Python 3.11.15, 3.12.13 and 3.14.3 on Windows through uv, and they behaved the same on all three. The explanation follows the 3.14 data model reference, the Descriptor Guide and the CPython 3.14 source, and it only looks at defaults, not at how much memory __slots__ saves.