Skip to main content
The 2026 Annual Developer Survey is live— take the Survey today!
6 of 6
added 1026 characters in body
Beni Cherniavsky-Paskin

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:

  1. normalizes negative start, stop by adding len(seq) – only once
  2. clips them to begging/end if still out of bounds
  3. 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.
Beni Cherniavsky-Paskin