Are __dict__ and __weakref__ the default slots at class definition?

According to the Python language documentation:

__slots__ allow us to explicitly declare data members (like properties) and deny the creation of __dict__ and __weakref__ (unless explicitly declared in __slots__ or available in a parent.)

So I am wondering if this class

>>> class A: __slots__ = ('__dict__', '__weakref__')
... 
>>> del A.__slots__
>>> vars(A)
mappingproxy({
    '__module__': '__main__',
    '__dict__': <attribute '__dict__' of 'A' objects>,
    '__weakref__': <attribute '__weakref__' of 'A' objects>,
    '__doc__': None
})

is equivalent to this one:

>>> class A: pass
... 
>>> vars(A)
mappingproxy({
    '__module__': '__main__',
    '__dict__': <attribute '__dict__' of 'A' objects>,
    '__weakref__': <attribute '__weakref__' of 'A' objects>,
    '__doc__': None
})

Answer

If you don’t set a __slots__ attribute, for entirely pure-Python classes (with only object as a native base), the default is to create the __weakref__ and __dict__ slots if no parent class has defined these already.

This happens in type.__new__(); a class is given descriptors to access memory slots for each instance:

  • First a check is made if the type can have these slots by testing for the tp_dictoffset, tp_weaklistoffset and tp_itemsize values in the type object struct.

    • You can only define a __dict__ slot, if the base class doesn’t not already define one (as indicated by tp_dictoffset).
    • You can only have a __weakref__ slot, of the base type doesn’t define one (via tp_weaklistoffset), and instance sizes must be fixed (tp_itemsize is zero).
  • then, if there is no __slots__, flag variables are set to actually enable the slots being added.

The type.__new__() implementation is long, but the relevant section looks like this:

    slots = _PyDict_GetItemIdWithError(dict, &PyId___slots__);
    nslots = 0;
    add_dict = 0;
    add_weak = 0;
    may_add_dict = base->tp_dictoffset == 0;
    may_add_weak = base->tp_weaklistoffset == 0 && base->tp_itemsize == 0;
    if (slots == NULL) {
        if (PyErr_Occurred()) {
            goto error;
        }
        if (may_add_dict) {
            add_dict++;
        }
        if (may_add_weak) {
            add_weak++;
        }
    }
    else {
        /* Have slots */
        ...
    }

base->tp_dictoffset is not 0 if the base defines a __dict__ slot, base->tp_weaklistoffset is not 0 if the base includes __weakref__ already, and base->tp_itemsize is 0 if the object has a fixed instance size.

Then, some 400 lines lower down, you’ll find the code that actually creates the slots:

    if (add_dict) {
        if (base->tp_itemsize)
            type->tp_dictoffset = -(long)sizeof(PyObject *);
        else
            type->tp_dictoffset = slotoffset;
        slotoffset += sizeof(PyObject *);
    }
    if (add_weak) {
        assert(!base->tp_itemsize);
        type->tp_weaklistoffset = slotoffset;
        slotoffset += sizeof(PyObject *);
    }

When extending certain native types (such as int or bytes) you don’t get a __weakref__ slot as they are of variable length (handled by tp_itemsize).