This issue tracker has been migrated to GitHub, and is currently read-only.
For more information, see the GitHub FAQs in the Python's Developer Guide.

classification
Title: __slots__ updates despite being read-only
Type: behavior Stage: resolved
Components: Interpreter Core Versions: Python 3.11, Python 3.10, Python 3.9
process
Status: closed Resolution: not a bug
Dependencies: Superseder:
Assigned To: Nosy List: IanLee1521, eryksun, ronaldoussoren, sobolevn
Priority: normal Keywords:

Created on 2022-01-27 14:55 by IanLee1521, last changed 2022-04-11 14:59 by admin. This issue is now closed.

Messages (9)
msg411886 - (view) Author: Ian Lee (IanLee1521) * Date: 2022-01-27 14:55
Hi there - I admit that I don't really understand the internals here, so maybe there is a good reason for this, but I thought it was weird when I just ran across it. 

If I create a new class `A`, and set it's `__slots`:

```python
➜  ~ docker run -it python:3.10
Python 3.10.2 (main, Jan 26 2022, 20:07:09) [GCC 10.2.1 20210110] on linux
Type "help", "copyright", "credits" or "license" for more information.

>>> class A(object):
...     __slots__ = ["foo"]
...
>>> A.__slots__
['foo']
```


If I then go to add a new attribute to extend it on the class, that works:

```python
>>> A.__slots__ += ["bar"]
>>> A.__slots__
['foo', 'bar']
```

But then if I create an instance of that class, and try to update `__slots__` on that instnace, I get an AttributeError that `__slots__` is read-only, and yet it still is updating the `__slots__` variable:

```python
>>> a = A()
>>> a.__slots__
['foo', 'bar']

>>> a.__slots__ += ["baz"]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'A' object attribute '__slots__' is read-only

>>> a.__slots__
['foo', 'bar', 'baz']
>>> A.__slots__
['foo', 'bar', 'baz']
```

Maybe there is a good reason for this, but I was definitely surprised that I would get a "this attribute is read-only" error, and yet still see that attribute updated.

I first found this in python 3.8.5, but I also tested using docker to generate the above example using docker python:3.10 which gave me python 3.10.2.

Cheers!
msg411889 - (view) Author: Nikita Sobolev (sobolevn) * (Python triager) Date: 2022-01-27 15:26
It does not happen on `3.11` (main):

```
Python 3.11.0a4+ (heads/main-dirty:ef3ef6fa43, Jan 20 2022, 20:48:25) [Clang 11.0.0 (clang-1100.0.33.16)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> class A:
...   __slots__ = ('x',)
... 
>>> A.__slots__ += ('y',)
>>> A.__slots__
('x', 'y')

>>> a = A()
>>> a.__slots__ 
('x', 'y')
>>> a.__slots__ += ('z',)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'A' object attribute '__slots__' is read-only

>>> a.__slots__
('x', 'y')
```

It also does not happen on `3.9`:

```
Python 3.9.9 (main, Dec 21 2021, 11:35:28) 
[Clang 11.0.0 (clang-1100.0.33.16)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> class A:
...   __slots__ = ('x',)
... 
>>> a = A()
>>> a.__slots__ += ('y',)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'A' object attribute '__slots__' is read-only

>>> a.__slots__
('x',)
```
msg411890 - (view) Author: Ian Lee (IanLee1521) * Date: 2022-01-27 15:33
@sobolevn - Hmm, interesting.. I tested in python 3.9 which I had available, and I can reproduce your result, but I think it's different because you are using a tuple. If I use a list then I see my same reported behavior in 3.9:


```python
Python 3.9.10 (main, Jan 26 2022, 20:56:53)
[GCC 10.2.1 20210110] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> class A:
...     __slots__ = ('x',)
...
>>> a = A()
>>> a.__slots__
('x',)
>>> a.__slots__ += ('y',)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'A' object attribute '__slots__' is read-only
>>> a.__slots__
('x',)
>>>
>>>
>>>
>>> class B:
...     __slots__ = ['x']
...
>>> b = B()
>>> b.__slots__
['x']
>>> b.__slots__ += ['y']
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'B' object attribute '__slots__' is read-only
>>> b.__slots__
['x', 'y']
```
msg411891 - (view) Author: Eryk Sun (eryksun) * (Python triager) Date: 2022-01-27 15:48
Please read about augmented assignment [1]. In the REPL, use help("+=").

    An augmented assignment evaluates the target (which, unlike normal
    assignment statements, cannot be an unpacking) and the expression
    list, performs the binary operation specific to the type of
    assignment on the two operands, and assigns the result to the
    original target. The target is only evaluated once.

    An augmented assignment expression like x += 1 can be rewritten as
    x = x + 1 to achieve a similar, but not exactly equal effect. In
    the augmented version, x is only evaluated once. Also, when
    possible, the actual operation is performed in-place, meaning that
    rather than creating a new object and assigning that to the target,
    the old object is modified instead.

    Unlike normal assignments, augmented assignments evaluate the left-
    hand side before evaluating the right-hand side. For example,
    a[i] += f(x) first looks-up a[i], then it evaluates f(x) and
    performs the addition, and lastly, it writes the result back to
    a[i].

---
[1] https://docs.python.org/3/reference/simple_stmts.html#augmented-assignment-statements
msg411984 - (view) Author: Ronald Oussoren (ronaldoussoren) * (Python committer) Date: 2022-01-28 10:47
Python's is behaving as expected here (but see below): the slots definition tells the interpreter which attribute names can be set on an instance and "__slots__" is not one of those attributes in your code.  "a.__slots__ += ..." will try to set the "a.__slots__" attribute (see Eryk's message for documentation on this) and that results in the exception you are seeing. 

What surprised me is that A.__slots__ is mutable at all, the value of that attribute during class construction affects the layout of instances and that layout won't change when you change A.__slots__ later on.

That is:

class A:
   __slots__ = ['a']

a = A()
a.a = ... # OK
a.b = ... # raises AttributeError

A.__slots__ = ['b']
a.a = ... # still OK
a.b = ... # still raises AttributeError

I don't know if this should be considered a bug or that this is intended behaviour.
msg412001 - (view) Author: Ian Lee (IanLee1521) * Date: 2022-01-28 15:10
@ronaldoussoren - right, I agree that I think that raising the AttributeErrors is the right thing. The part that feels like a bug to me is that the exception is saying it is read only and yet it is not being treated it that way (even though as you point out, the end result doesn't "work").

Maybe this is something about the augmented assignment that I'm just not grokking... I read the blurb @eryksun posted several times, but not seeming to see what is going on.
msg412009 - (view) Author: Eryk Sun (eryksun) * (Python triager) Date: 2022-01-28 16:54
If the target object of an augmented assignment doesn't support the in-place binary operation, the normal binary operation is used instead. Thus an augmented assignment is implemented to always assign the result back to the target. For an attribute, that's similar to `x.a += 1` -> `x.a = x.a + 1`. For example:

    >>> dis.dis('x.a += 1')
                  0 RESUME                   0

      1           2 LOAD_NAME                0 (x)
                  4 DUP_TOP
                  6 LOAD_ATTR                1 (a)
                  8 LOAD_CONST               0 (1)
                 10 BINARY_OP               13 (+=)
                 12 ROT_TWO
                 14 STORE_ATTR               1 (a)
                 16 LOAD_CONST               1 (None)
                 18 RETURN_VALUE

Note the STORE_ATTR instruction in the above bytecode.

As to __slots__, I think that class construction should store it as a tuple, unless maybe I'm overlooking some use case.
msg412371 - (view) Author: Ronald Oussoren (ronaldoussoren) * (Python committer) Date: 2022-02-02 16:09
W.r.t. augmented assignments, this is a good blog post about how they work under the hood: https://snarky.ca/unravelling-augmented-arithmetic-assignment/
msg412394 - (view) Author: Ronald Oussoren (ronaldoussoren) * (Python committer) Date: 2022-02-02 20:09
There's a use case for using a dict for __slots__, pydoc will use a dict value for the slots as a mapping from attribute name to attribute documentation.

Because of that it is not really worthwhile to transform the value of __slots__ during class definition.
History
Date User Action Args
2022-04-11 14:59:55adminsetgithub: 90708
2022-02-02 20:09:27ronaldoussorensetstatus: open -> closed
resolution: not a bug
messages: + msg412394

stage: resolved
2022-02-02 16:09:04ronaldoussorensetmessages: + msg412371
2022-01-28 16:54:05eryksunsetmessages: + msg412009
2022-01-28 15:10:19IanLee1521setmessages: + msg412001
2022-01-28 10:47:46ronaldoussorensetversions: + Python 3.9, Python 3.11, - Python 3.8
nosy: + ronaldoussoren

messages: + msg411984

components: + Interpreter Core, - Library (Lib)
2022-01-27 15:48:04eryksunsetnosy: + eryksun
messages: + msg411891
2022-01-27 15:33:20IanLee1521setmessages: + msg411890
2022-01-27 15:26:28sobolevnsetnosy: + sobolevn
messages: + msg411889
2022-01-27 14:55:32IanLee1521create