Immutable or mutable?

The concepts of mutability explained by 3. Data model — Python 3.12.1 documentation

however, when we talk about the mutability of a container, only the identities of the immediately contained objects are implied. So, if an immutable container (like a tuple) contains a reference to a mutable object, its value changes if that mutable object is changed.`

Yes, it is changed but may lead an error!
Below code sample leads a bit of confusion. (originally by Leonardo Rochael and Cesar Kawakami for sharing this riddle at the 2013 PythonBrasil Conference, reposted in book Fluent Python rev.2)

>>> v = (1,3,[1,3])
>>> v[2] += [5,7]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
>>> v
(1, 3, [1, 3, 5, 7])
>>> v[2].append(5)
>>> v
(1, 3, [1, 3, 5, 7, 5])

We know the reason why TypeError is thrown with +=, if tracking down to disassemble level code to see details demonstrated by Fluent Python (page54-56, rev 2). But from a higher level aspect, += operator is NOTHING else but a list method function __iadd__(). What makes it different to another list method append() is from lower implementation level in CPython. It feels uncomfortable, somehow. Even we could keep this puzzle in mind to avoid surprising behavior when coding.

Not quite. It’s a list method AND an assignment.

>>> def f(x):
...     x += (1,)
... 
>>> dis.dis(f)
  1           0 RESUME                   0

  2           2 LOAD_FAST                0 (x)
              4 LOAD_CONST               1 ((1,))
              6 BINARY_OP               13 (+=)
             10 STORE_FAST               0 (x)
             12 RETURN_CONST             0 (None)

In effect, x += (1,) is equivalent to x = x.__iadd__((1,)) - it first offers the object a chance to do an in-place addition, and then, whether it does or not, assigns it back.

That’s why += works even on integers, which don’t do any sort of in-place operations, why it works just fine on strings but potentially either slow or fast depending on implementation, and why tuple assignment in this way bombs.

It is even more confusing, if checking a list that is not embedded in a container:
what STORE_NAME actually do?

>>> li = [1]
>>> li
[1]
>>> id(li)
1684087818752
>>> li += [2,3]
>>> id(li)
1684087818752
>>> dis.dis("li += [2,3]")
  1           0 LOAD_NAME                0 (li)
              2 LOAD_CONST               0 (2)
              4 LOAD_CONST               1 (3)
              6 BUILD_LIST               2
              8 INPLACE_ADD
             10 STORE_NAME               0 (li)
             12 LOAD_CONST               2 (None)
             14 RETURN_VALUE

OK, now I see how it varies comparing to append method. Thanks. Case can be closed.

>>> dis.dis("tu[2].append(6)")
  1           0 LOAD_NAME                0 (tu)
              2 LOAD_CONST               0 (2)
              4 BINARY_SUBSCR
              6 LOAD_METHOD              1 (append)
              8 LOAD_CONST               1 (6)
             10 CALL_METHOD              1
             12 RETURN_VALUE

The result is well known: