pythonsuperslotsreadonly-attribute

How to create read-only slots?


Slots are writable by default:

>>> class A: __slots__ = ('x',)
... 
>>> list(vars(A))
['__module__', '__slots__', 'x', '__doc__']
>>> vars(A)['x']
<member 'x' of 'A' objects>
>>> a = A()
>>> a.x = 'foo'
>>> del a.x

How to create read-only slots, like the slots '__thisclass__' , '__self__', and '__self_class__' of the class super?

>>> list(vars(super))
['__repr__', '__getattribute__', '__get__', '__init__', '__new__',
 '__thisclass__', '__self__', '__self_class__', '__doc__']
>>> vars(super)['__thisclass__']
<member '__thisclass__' of 'super' objects>
>>> s = super(int, 123)
>>> s.__thisclass__ = float
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: readonly attribute
>>> del s.__thisclass__
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: readonly attribute

Solution

  • You can't, there is no option from Python code to create read-only descriptors like the ones used for __thisclass__, etc.

    In the C API, slots all use the same descriptor object type, which become read-only for entries in PyMemberDef arrays that have flags set to READONLY.

    E.g. the super descriptors you identified are defined in the super_members array:

    static PyMemberDef super_members[] = {
        {"__thisclass__", T_OBJECT, offsetof(superobject, type), READONLY,
         "the class invoking super()"},
        {"__self__",  T_OBJECT, offsetof(superobject, obj), READONLY,
         "the instance invoking super(); may be None"},
        {"__self_class__", T_OBJECT, offsetof(superobject, obj_type), READONLY,
         "the type of the instance invoking super(); may be None"},
        {0}
    };
    

    When __slots__ is being processed, there is no path where you can set this flag; the code in type.__new__ just sets the first three values, which are name, type and offset, respectively:

        mp = PyHeapType_GET_MEMBERS(et);
        slotoffset = base->tp_basicsize;
        if (et->ht_slots != NULL) {
            for (i = 0; i < nslots; i++, mp++) {
                mp->name = PyUnicode_AsUTF8(
                    PyTuple_GET_ITEM(et->ht_slots, i));
                if (mp->name == NULL)
                    goto error;
                mp->type = T_OBJECT_EX;
                mp->offset = slotoffset;
    
    
                /* __dict__ and __weakref__ are already filtered out */
                assert(strcmp(mp->name, "__dict__") != 0);
                assert(strcmp(mp->name, "__weakref__") != 0);
    
    
                slotoffset += sizeof(PyObject *);
            }
        }
    

    For reference:

    Note that if it was possible to set these slots to read-only, you'd also need a mechanism to provide their value before creating a new instance! After all, if all Python code was prevented from setting a readonly attribute on instances, how would your Python class ever get to set the initial value?