I think the real explanation is that with += there are two steps:
1- The existing list gets modified. This is fine since lists are mutable.
2- The tuple's reference to the list gets updated (even though this update is unnecessary since the list object's identity is the same).
The exception occurs at step 2, but this step is otherwise a no-op. Whether mutating a reference to the same value it already had is an actual mutation is an interesting semantics debate.
When you want to use a tuple as a key in a dictionary, Python checks if all members of the tuple are immutable. If one or more of them aren't, you'll get an 'unhashable type' error.
list.__iadd__ is (sadly) defined as something like
def __iadd__(self, other):
self.extend(other)
return self
So it's possible to have __iadd__ itself succeed modifying the list in place but then the "no-op" reassignment fail.[0] like many data model operations there's really a bunch of fallbacks depending on what is and is not implemented
It's not exactly a bug but it is a somewhat unexpected behaviour and IIRC Guido regretted that list.__iadd__ was overridden this way.
> None of the other += operators returns a value.
It's not the operator which returns a value, it's the data model hook. The data model requires that it return a value: https://docs.python.org/3/reference/datamodel.html?#object._...
> These methods should attempt to do the operation in-place (modifying self) and return the result (which could be, but does not have to be, self).
Really the issue is that the "in-place" hooks are simply weird.