Skip to main content
AI Assist is now on Stack Overflow. Start a chat to get instant answers from across the network. Sign up to save and share your chats.
added 1026 characters in body
Source Link

Sequence slicing is same, except it first normalizes negative indexes, and it can never go outside the sequence:

  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()

TODOEDIT(2016): The code below had a bug with "never go outside the sequence" when abs(step)>1; I think I patched it"fixed" to be correctclip before stepping, but it's hard to understand.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 startstep is> None0:
        startFIRST, AFTER_LAST = (0, ifL
 step > 0 else:
 len(seq)       FIRST, AFTER_LAST = L-1, -1
    # Either way, range(FIRST, AFTER_LAST) covers whole seq

    elifif start <is 0None:
        start +== len(seq)FIRST
    if not 0 <=elif start < len(seq)0: 
  # clip if still outside bounds
 start += L  # only once, startcan =still (0be ifnegative
 step > 0 elsestart len= clip(seq)-1start, {FIRST, AFTER_LAST}) 

    if stop is None:
        stop = (len(seq)AFTER_LAST
 if step > elif stop < 0:
 else -1)      stop += L  # reallyonly -1once, notcan laststill element
be negative
   elif stop <= 0:
clip(stop, {FIRST, AFTER_LAST})

    print(f'Using range({start}, {stop +=}, len(seq{step})')
    for i in range(start, stop, step):
        ifyield 0seq[i]

def clip(n, bounds):
    """Clip so min(bounds) <= iresult <<= lenmax(seqbounds)."""
    low, high = sorted(bounds)
    if n < low:
        return low
   yield seq[i]if n > high:
        return high
    return n

Don't worry about the is None details - just remember that omitting start and/or stop always does the right thing to give you the whole sequence.

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.

Sequence slicing is same, except it first normalizes negative indexes, and it can never go outside the sequence:

TODO: The code below had a bug with "never go outside the sequence" when abs(step)>1; I think I patched it to be correct, but it's hard to understand.

def this_is_how_slicing_works(seq, start=None, stop=None, step=1):
    if start is None:
        start = (0 if step > 0 else len(seq)-1)
    elif start < 0:
        start += len(seq)
    if not 0 <= start < len(seq):  # clip if still outside bounds
        start = (0 if step > 0 else len(seq)-1)
    if stop is None:
        stop = (len(seq) if step > 0 else -1)  # really -1, not last element
    elif stop < 0:
        stop += len(seq)
    for i in range(start, stop, step):
        if 0 <= i < len(seq):
            yield seq[i]

Don't worry about the is None details - just remember that omitting start and/or stop always does the right thing to give you the whole sequence.

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.

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.
small python-2-ism: range() no longer == a list
Source Link

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 indexes, and it can never go outside the sequence:

TODO: The code below had a bug with "never go outside the sequence" when abs(step)>1; I think I patched it to be correct, but it's hard to understand.

def this_is_how_slicing_works(seq, start=None, stop=None, step=1):
    if start is None:
        start = (0 if step > 0 else len(seq)-1)
    elif start < 0:
        start += len(seq)
    if not 0 <= start < len(seq):  # clip if still outside bounds
        start = (0 if step > 0 else len(seq)-1)
    if stop is None:
        stop = (len(seq) if step > 0 else -1)  # really -1, not last element
    elif stop < 0:
        stop += len(seq)
    for i in range(start, stop, step):
        if 0 <= i < len(seq):
            yield seq[i]

Don't worry about the is None details - just remember that omitting start and/or stop always does the right thing to give you the whole sequence.

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.

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 indexes, and it can never go outside the sequence:

TODO: The code below had a bug with "never go outside the sequence" when abs(step)>1; I think I patched it to be correct, but it's hard to understand.

def this_is_how_slicing_works(seq, start=None, stop=None, step=1):
    if start is None:
        start = (0 if step > 0 else len(seq)-1)
    elif start < 0:
        start += len(seq)
    if not 0 <= start < len(seq):  # clip if still outside bounds
        start = (0 if step > 0 else len(seq)-1)
    if stop is None:
        stop = (len(seq) if step > 0 else -1)  # really -1, not last element
    elif stop < 0:
        stop += len(seq)
    for i in range(start, stop, step):
        if 0 <= i < len(seq):
            yield seq[i]

Don't worry about the is None details - just remember that omitting start and/or stop always does the right thing to give you the whole sequence.

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.

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 indexes, and it can never go outside the sequence:

TODO: The code below had a bug with "never go outside the sequence" when abs(step)>1; I think I patched it to be correct, but it's hard to understand.

def this_is_how_slicing_works(seq, start=None, stop=None, step=1):
    if start is None:
        start = (0 if step > 0 else len(seq)-1)
    elif start < 0:
        start += len(seq)
    if not 0 <= start < len(seq):  # clip if still outside bounds
        start = (0 if step > 0 else len(seq)-1)
    if stop is None:
        stop = (len(seq) if step > 0 else -1)  # really -1, not last element
    elif stop < 0:
        stop += len(seq)
    for i in range(start, stop, step):
        if 0 <= i < len(seq):
            yield seq[i]

Don't worry about the is None details - just remember that omitting start and/or stop always does the right thing to give you the whole sequence.

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.

Active reading.
Source Link
Peter Mortensen
  • 31.4k
  • 22
  • 110
  • 134

I find it easier to remember how it'sit 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):  # illegalIllegal 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 indexes, and it can never go outside the sequence:

TODO: The code below had a bug with "never go outside the sequence" when abs(step)>1; I think I patched it to be correct, but it's hard to understand.

def this_is_how_slicing_works(seq, start=None, stop=None, step=1):
    if start is None:
        start = (0 if step > 0 else len(seq)-1)
    elif start < 0:
        start += len(seq)
    if not 0 <= start < len(seq):  # clip if still outside bounds
        start = (0 if step > 0 else len(seq)-1)
    if stop is None:
        stop = (len(seq) if step > 0 else -1)  # really -1, not last element
    elif stop < 0:
        stop += len(seq)
    for i in range(start, stop, step):
        if 0 <= i < len(seq):
            yield seq[i]

Don't worry about the is None details - just remember that omitting start and/or stop always does the right thing to give you the whole sequence.

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.

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=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 indexes, and can never go outside the sequence:

TODO: The code below had a bug with "never go outside the sequence" when abs(step)>1; I think I patched it to be correct, but it's hard to understand.

def this_is_how_slicing_works(seq, start=None, stop=None, step=1):
    if start is None:
        start = (0 if step > 0 else len(seq)-1)
    elif start < 0:
        start += len(seq)
    if not 0 <= start < len(seq):  # clip if still outside bounds
        start = (0 if step > 0 else len(seq)-1)
    if stop is None:
        stop = (len(seq) if step > 0 else -1)  # really -1, not last element
    elif stop < 0:
        stop += len(seq)
    for i in range(start, stop, step):
        if 0 <= i < len(seq):
            yield seq[i]

Don't worry about the is None details - just remember that omitting start and/or stop always does the right thing to give you the whole sequence.

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.

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 indexes, and it can never go outside the sequence:

TODO: The code below had a bug with "never go outside the sequence" when abs(step)>1; I think I patched it to be correct, but it's hard to understand.

def this_is_how_slicing_works(seq, start=None, stop=None, step=1):
    if start is None:
        start = (0 if step > 0 else len(seq)-1)
    elif start < 0:
        start += len(seq)
    if not 0 <= start < len(seq):  # clip if still outside bounds
        start = (0 if step > 0 else len(seq)-1)
    if stop is None:
        stop = (len(seq) if step > 0 else -1)  # really -1, not last element
    elif stop < 0:
        stop += len(seq)
    for i in range(start, stop, step):
        if 0 <= i < len(seq):
            yield seq[i]

Don't worry about the is None details - just remember that omitting start and/or stop always does the right thing to give you the whole sequence.

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.

temp fix(?) for clipping bug with step>1
Source Link
Loading
defaults of omitted start/stop
Source Link
Loading
Source Link
Loading