Tuple slicing not returning a new object as opposed to list slicing

It's an implementation detail. Because lists are mutable, l1[:] must create a copy, because you wouldn't expect changes to l2 to affect l1.

Since a tuple is immutable, though, there's nothing you can do to t2 that would affect t1 in any visible way, so the compiler is free (but not required) to use the same object for t1 and t1[:].

Implementations are free to return identical instances for immutable types (in CPython, you may sometimes see similar optimizations for strings and integers). Since the object can not be changed, there is nothing in user code that needs to care whether it holds a unique instance or just another reference to an existing instance.

You can find the short-circuit in the C code here.

static PyObject*
tuplesubscript(PyTupleObject* self, PyObject* item)
    ... /* note: irrelevant parts snipped out */
    if (start == 0 && step == 1 &&
                 slicelength == PyTuple_GET_SIZE(self) &&
                 PyTuple_CheckExact(self)) {
            Py_INCREF(self);          /* <--- increase reference count */
            return (PyObject *)self;  /* <--- return another pointer to same */

This is an implementation detail, note that pypy does not do the same.