I find it easier to remember how it's works, then I can figure out any specific start/stop/step combination.
It's instructive to understand range() first:
def range(start, stop, step):
i = start
while (i < stop if step > 0 else i > stop):
yield i
i += step
Begin from start, increment by step, do not reach stop. Very simple.
The thing to remember about negative step is that stop is always the excluded end, whether it's higher or lower. If you want same slice in opposite order, it's much cleaner to do the reversal separately: e.g. 'abcde'[1:-2][::-1] slices off one char from left, two from right, then reverses. (See also reversed().)
Sequence slicing is same, except it first normalizes negative indexes, and can never go outside the sequence:
def this_is_how_slicing_works(seq, start, stop, step):
if start < 0:
start += len(seq)
if stop < 0:
stop += len(seq)
for i in range(start, stop, step):
if 0 <= i < len(seq):
yield seq[i]
Normalizing negative indexes first allows start and/or stop to be counted from the end independently: 'abcde'[1:-2] == 'abcde'[1:3] == 'bc' despite range(1,-2) == [].
The normalization is sometimes thought of as "modulo the length" but note it adds the length just once: e.g. 'abcde'[-53:42] is just the whole string.