I thought I had the whole list alias thing figured out, but then I came across this:
l = [1, 2, 3, 4]
for i in l:
i = 0
print(l)
<
i = 0 is very different from i[0] = 0.
Ignacio has explained the reasons, succinctly and correctly. So I'll just try to explain in a more simple words what's actually going on here.
In the first case, i is just a label pointing at some object (one of the members in your list). i = 0 changes the reference to some other object, so that i now references the integer 0. The list is unmodified, because you never asked to modify l[0] or any element of l, you only modified i.
In the second case, i is also just a name pointing at one of the members in your list. That part is no different. However, i[0] is now calling .__getitem__(0) on one of the list members. Similarly, i[0] = 'other' would be like doing i.__setitem__(0, 'other'). It is not simply pointing i at a different object, as a regular assignment statement would, actually it's mutating the object i.
An easy way to think of it is always that names in Python are just labels for objects. A scope or namespace is just like a dict mapping names to objects.