When you assigned a to b first, you only have one actual list data on the heap, you should try to assign another list data on heap to b first.

In your example, when you first assign ['h', 'e', 'l', 'l', 'o'] to a, you are creating a variable a pointing to the actual list data on heap. After that you assign a to b, which means creating a variable b pointing to the data that the a is pointing to, now the structure looks like this:
a ---> ['h', 'e', 'l', 'l', 'o']
^
|
b ------
Then, b[-1] = 'q' means change the last element of the list data which b is pointing to, which results in:
a ---> ['h', 'e', 'l', 'l', 'q']
^
|
b ------
After that, b[:] = a means change the data of the list to which b is pointing to the data of the list to which a is pointing, which in this situation does nothing (a and b are still pointing to the same list data). Therefore, the following a[-1] = 'Q' and b[-1] = 'o' are just changing the (only) list data, which looks like:
a ---> ['h', 'e', 'l', 'l', 'Q']
^
|
b ------
a ---> ['h', 'e', 'l', 'l', 'o']
^
|
b ------
Back to the difference of b = a and b[:] = a, if you want to see how b[:] = a is different from b = a, you could first assign ['h', 'e', 'l', 'l', 'o'] to a and assign another list [] to b, now the structure looks like:
a ---> ['h', 'e', 'l', 'l', 'o']
b ---> []
Then if you use b[:] = a, which change the data of the list to which b is pointing to the data of the list to which a is pointing, you will get a structure like:
a ---> ['h', 'e', 'l', 'l', 'o']
b ---> ['h', 'e', 'l', 'l', 'o']
In this state, any change you made to the list to which a is pointing won't affect the list to which b is pointing. For instance, use a[-1] = 'Q' now will only change the list to which a is pointing, which results in a structure like:
a ---> ['h', 'e', 'l', 'l', 'Q']
b ---> ['h', 'e', 'l', 'l', 'o']