In addition to Joran's answer, I'd point you to this reference implementation, confirming his answer that it is O(1) lookup
/* String slice a[i:j] consists of characters a[i] ... a[j-1] */
static PyObject *
string_slice(register PyStringObject *a, register Py_ssize_t i,
register Py_ssize_t j)
/* j -- may be negative! */
{
if (i < 0)
i = 0;
if (j < 0)
j = 0; /* Avoid signed/unsigned bug in next line */
if (j > Py_SIZE(a))
j = Py_SIZE(a);
if (i == 0 && j == Py_SIZE(a) && PyString_CheckExact(a)) {
/* It's the same as a */
Py_INCREF(a);
return (PyObject *)a;
}
if (j < i)
j = i;
return PyString_FromStringAndSize(a->ob_sval + i, j-i);
}
Why this should be your intuition
Python strings are immutable. This common optimization allows tricks like assuming contiguous data when desirable. Note that under the hood, we sometimes just need to compute the offset from the memory location in C (obviously implementation specific)
There are several places where the immutability of strings is something that can be relied on (or vexed by). In the python author's words;
There are several advantages [to strings being immutable]. One is
performance: knowing that a string is immutable means we can allocate
space for it at creation time
So although we may not be able to guarantee, as far as I know, this behaviour across implementations, it's awfully safe to assume.