"Mutable" means that you can change a particular element within the object. With a nonmutable object, if you want a different value for a particular element, you have to re-assign the entire object. Lists always have the capability of changing a particular element. In your second example, the list is still mutable, but you aren't mutating it. you're re-assigning it.
In Python, when a function is called, the default behavior is for the scope of all variables names to be just within the function, but the function has full access to the objects. When you write x[0] = 2
, you're accessing the actual object, and changing the first element of that object. When you write x = [2]
, you're no longer dealing with the same object, you're only dealing with the name "x", and the scope of the name is only within the function. If you had written global x
in the function, however, then that would tell Python that you want the name "x" to have global scope, and it would reassign the name "x" to the new object [2]
for the entire program. You would then see a different value for both x[0]
and id(x)
in the main program.
It's a rather difficult thing to explain, because the issue is the distinction between the name of an object versus the object that that name refers to, and it's difficult to talk about objects without using their names. I don't know if this makes things clearer or is just more confusing, but suppose we call the first list object1, and the second one object2 (that is, we'll pretend that if you take the id of the first object, you get 1, and if you take the id of the second one, you get 2). When you call test
, you pass object1 with the name "x". If you have the line x=[2]
, you're creating object2, putting the value 2
in it, and telling Python that within the function test
, the name x
points to object2. If you have the line x[0]=2
, on the other hand, you're still dealing with object1, and you're changing the value of its first element to 2
.