Why was p[:] designed to work differently in these two situations?

As others have stated; p[:] deletes all items in p; BUT will not affect q. To go into further detail the list docs refer to just this:

All slice operations return a new list containing the requested elements. This means that the following slice returns a new (shallow) copy of the list:

>>> squares = [1, 4, 9, 16, 25]
...
>>> squares[:]
[1, 4, 9, 16, 25]

So q=p[:] creates a (shallow) copy of p as a separate list but upon further inspection it does point to a completely separate location in memory.

>>> p = [1,2,3]
>>> q=p[:]
>>> id(q)
139646232329032
>>> id(p)
139646232627080

This is explained better in the copy module:

A shallow copy constructs a new compound object and then (to the extent possible) inserts references into it to the objects found in the original.

Although the del statement is performed recursively on lists/slices:

Deletion of a target list recursively deletes each target, from left to right.

So if we use del p[:] we are deleting the contents of p by iterating over each element, whereas q is not altered as stated earlier, it references a separate list although having the same items:

>>> del p[:]
>>> p
[]
>>> q
[1, 2, 3]

In fact this is also referenced in the list docs as well in the list.clear method:

list.copy()

Return a shallow copy of the list. Equivalent to a[:].

list.clear()

Remove all items from the list. Equivalent to del a[:].


del and assignments are designed consistently, they're just not designed the way you expected them to be. del never deletes objects, it deletes names/references (object deletion only ever happens indirectly, it's the refcount/garbage collector that deletes the objects); similarly the assignment operator never copies objects, it's always creating/updating names/references.

The del and assignment operator takes a reference specification (similar to the concept of an lvalue in C, though the details differs). This reference specification is either a variable name (plain identifier), a __setitem__ key (object in square bracket), or __setattr__ name (identifier after dot). This lvalue is not evaluated like an expression, as doing that will make it impossible to assign or delete anything.

Consider the symmetry between:

p[:] = [1, 2, 3]

and

del p[:]

In both cases, p[:] works identically because they are both evaluated as an lvalue. On the other hand, in the following code, p[:] is an expression that is fully evaluated into an object:

q = p[:]

del on iterator is just a call to __delitem__ with index as argument. Just like parenthesis call [n] is a call to __getitem__ method on iterator instance with index n.

So when you call p[:] you are creating a sequence of items, and when you call del p[:] you map that del/__delitem__ to every item in that sequence.