I find it easier to remember how it works, and then I can figure out any specific start/stop/step combination.
It's instructive to understand range() first:
def range(start=0, stop, step=1): # Illegal syntax, but that's the effect
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
start,stopby addinglen(seq)– only once - clips them to begging/end if still out of bounds
- and then it steps, like range()
EDIT(2016): "fixed" to clip before stepping, but with new bug
EDIT(2025): fixed again, now with tests here
def this_is_how_slicing_works(seq, start=None, stop=None, step=1):
L = len(seq)
if step > 0:
FIRST, AFTER_LAST = 0, L
else:
FIRST, AFTER_LAST = L-1, -1
# Either way, range(FIRST, AFTER_LAST) covers whole seq
if start is None:
start = FIRST
elif start < 0:
start += L # only once, can still be negative
start = clip(start, {FIRST, AFTER_LAST})
if stop is None:
stop = AFTER_LAST
elif stop < 0:
stop += L # only once, can still be negative
stop = clip(stop, {FIRST, AFTER_LAST})
print(f'Using range({start}, {stop}, {step})')
for i in range(start, stop, step):
yield seq[i]
def clip(n, bounds):
"""Clip so min(bounds) <= result <= max(bounds)."""
low, high = sorted(bounds)
if n < low:
return low
if n > high:
return high
return n
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) being empty.
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.
The FIRST, AFTER_LAST = L-1, -1 case is messy, but they have to be off by -1 to cover whole range in reverse (range(100)[::-1] == range(99, -1, -1)).
Don't worry, just remember that omitting start and/or stop always does the right thing to give you the whole sequence.
- See aguadopd's answer for details of how negative step breaks the "between items" mental model; IMHO negative step is hopelessly confusing and I strongly recommend reversing and cutting in separate operations e.g.
a[10:-5][::-1]. - See https://stackoverflow.com/a/71330285/239657 explaining CPython's actual implementation of slicing.