I don’t understand slicing with negative bounds in Python. How is this supposed to work?

Each Answer to this Q is separated by one/two green lines.

I am a newbie to Python and have come across the following example in my book that is not explained very well. Here is my print out from the interpreter:

>>> s="spam"
>>> s[:-1]
'spa'

Why does slicing with no beginning bound and a '-1' return every element except the last one? Is calling s[0:-1] logically the same as calling s[:-1]? They both return the same result. But I’m not sure what python is doing exactly. Any help would be greatly appreciated.

Yes, calling s[0:-1] is exactly the same as calling s[:-1].

Using a negative number as an index in python returns the nth element from the right-hand side of the list (as opposed to the usual left-hand side).

so if you have a list as so:

myList = ['a', 'b', 'c', 'd', 'e']
print myList[-1] # prints 'e'

the print statement will print “e”.

Once you understand that (which you may already, it’s not entirely clear if that’s one of the things you’re confused about or not) we can start talking about slicing.

I’m going to assume you understand the basics of a slice along the lines of myList[2:4] (which will return ['c', 'd']) and jump straight into the slicing notation where one side is left blank.

As you suspected in your post, myList[:index] is exactly the same as myList[0:index].

This is also works the other way around, by the way… myList[index:] is the same as myList[index:len(myList)] and will return a list of all the elements from the list starting at index and going till the end (e.g. print myList[2:] will print ['c', 'd', 'e']).

As a third note, you can even do print myList[:] where no index is indicated, which will basically return a copy of the entire list (equivalent to myList[0:len(myList)], returns [‘a’, ‘b’, ‘c’, ‘d’, ‘e’]). This might be useful if you think myList is going to change at some point but you want to keep a copy of it in its current state.

If you’re not already doing it I find just messing around in a Python interpreter a whole bunch a big help towards understanding these things. I recommend IPython.

>>> l = ['abc', 'def', 'ghi', 'jkl', 'mno', 'pqr', 'stu', 'vwx', 'yz&']

# I want a string up to 'def' from 'vwx', all in between
# from 'vwx' so -2;to 'def' just before 'abc' so -9; backwards all so -1.
>>> l[-2:-9:-1]
['vwx', 'stu', 'pqr', 'mno', 'jkl', 'ghi', 'def']

# For the same 'vwx' 7 to 'def' just before 'abc' 0, backwards all -1
>>> l[7:0:-1]
['vwx', 'stu', 'pqr', 'mno', 'jkl', 'ghi', 'def']

Please do not become listless about list.

  1. Write the first element first. You can use positive or negative index
    for that. I am lazy so I use positive, one stroke less (below 7, or -3 for the start).
  2. Index of the element just before where you want to stop.
    Again, you can use positive or negative index for that (below 2 or -8 for stop).
  3. Here sign matters; of course – for backwards; value of stride you know.
    Stride is a ‘vector’ with both magnitude and direction (below -1, backwards all).

    l = [0,1,2,3,4,5,6,7,8,9]
    l[7:2:-1], l[-3:2:-1], [-3:-8:-1],l[7:-8:-1]
    

    All result in [7, 6, 5, 4, 3].

Negative indices are counted from the end, so s[:-1] is equivalent to s[:len(s)-1] and s[-1] is the last element, for example.

The crucial point is that python indices should be thought of as pointers to the spaces between the entries in a list, rather than to the elements themselves. Hence, 0 points to the beginning, 1 to between the first and second, … and n to between the nth and (n+1)st.

Thus l[1:2] gives you a list containing just element l[1] since it gives you everything between the two pointers.

Similarly, negative indices point in between elements, but this time counting from the back, so -1 points between the last element and the next-to-last, so [0:-1] refers to a block of items not including that last one.

As syntactic sugar, you can leave off 0 from the beginning or, in effect, the end, so l[n:] refers to everything from l[n] to the end (if n>=len(l) then it returns the empty list).

Yes, calling s[0:-1] is logically the same thing as s[:-1] since slicing is best defined as:

[beginning_index:ending_index]

Python allows you to omit 0 as this allows your code to more terse.

I’ll be addressing a point that some others have missed:

How do we interpret this negative index, in the context of what we know about slicing?

Generally, when we do a slice, we talk about [inclusive, exclusive] bounds. So

A = [1,3,4,6]
A[1:3] # will give us 3-1 = 2 elements, index 1 and 2 => [3,4]

So when we have a negative index in the slice, A[1:-1], this means we have A[1:len(A)-1] = A[1:3] which gives us again index 1 and 2 and hence [3,4].

Note that even though the list has length 4, its last index is 3, hence why this -1 notation will work here. Also note that if you have code that takes in the negative index as a variable, then you’ll need to manually check for 0, since
A[:-0] == A[:0] == []
`

If we want to print from the back-end of the string we can go for negative indexing. Indexing starts from -1.

Example : s=”hello world”

s[-11:-1] = ‘hello worl’
s[-1:-11] = ” // beginning value should be lower(i.e., in this case -1 greater than -11) if it is greater it won’t print anything.

RULE [START, END, STEP] or [START, END]

  • If you want a slice in regular order, STEP should be positive.

  • If you want a slice in reverse order, STEP should be negative.

eg:

>>> s="abcdefg"
>>> s[1:6:1]
'bcdef'     
>>> s[6:1:1]
''     
# If in regular order, s[6:1]='' therefore same for s[6:1:1]
>>> s[6:1:-1]
'gfedc'
# Reverse order, START=6 and END=1, a slice is possible

What Python is exactly doing while slicing a list is coded in sliceobject.c file of Python source code. If you are not in trouble to clearly see how it comes that none of the lines of code below gives an Exception or Error or you are not surprised by the outcome of slicing:

assert [0,1,2,3][-23: 32]    == [0,1,2,3]
assert [0,1,2,3][ 23: 32]    == []
assert [0,1,2,3][-32:-23]    == []
assert [0,1,2,3][ 23:-23:-1] == [3,2,1,0]
# ---
assert [0,1,2,3][ -1: 3:-1] == []
assert [0,1,2,3][ -1: 2:-1] == [3]
assert [0,1,2,3][ -1: 1:-1] == [3,2]
assert [0,1,2,3][ -1: 0:-1] == [3,2,1]
assert [0,1,2,3][ -1:-1:-1] == []
assert [0,1,2,3][ -1:-2:-1] == [3]
assert [0,1,2,3][ -1:-3:-1] == [3,2]
assert [0,1,2,3][ -1:-4:-1] == [3,2,1]
assert [0,1,2,3][ -1:-5:-1] == [3,2,1,0]
# ---
assert [0,1,2,3][321:-123: 1] == []
assert [0,1,2,3][321:-123:-1] == [3,2,1,0]
assert [0,1,2,3][-123:321:-1] == []
# ---
assert [0,1][None:None][None:][::][:] == [0,1]

there is good chance that you had already well understood how Python slicing work.

If you were surprised by some of the given examples, looking into the Python source code may help to resolve the confusion. I have rewritten the C-function returning start, stop, step values for range() which can then be used to create a slice of a listL with:

[ listL[indx] for indx in range(start,stop, step) ]

in Python and provided it below. It can be studied in detail to get some insight into the mechanisms behind slicing a list. I hope it will help you to gain understanding of the at the first glance maybe surprising or hard to grasp results with negative step and index while slicing a list.

By the way: if you ever wondered how to reach the first element of a list while using a negative step, here some options how it can be done:

listL = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
assert listL[3:   2: -1] == [3]
assert listL[3:   1: -1] == [3, 2]
assert listL[3:   0: -1] == [3, 2, 1]
...
assert listL[3: -11: -1] == [3, 2, 1, 0]
assert listL[3:None: -1] == [3, 2, 1, 0]
assert listL[3::-1]      == [3, 2, 1, 0]
assert listL[3:-321: -1] == [3, 2, 1, 0]

Below the Python script code equivalent to a part of the C-code in sliceobject.c file which handles slicing. Notice that missing values in [::] are turned into None for the evaluation of the slice indices. I have provided some comments in the code to help grasping what and why:

# Python-3.9.10_srcCode/Objects/sliceobject.c:354:
#         evaluate_slice_index(PyObject *v)
def evaluate_slice_index(v):
    if v is None:
        return None
    if type(v) is int:
        return v
    if '__index__' in dir(v):
        return v.__index__()
    else:
        raise TypeError(
  "slice indices must be integers or None or have an __index__ method")
#:def 

# Python-3.9.10_srcCode/Objects/sliceobject.c:372:
#         _PySlice_GetLongIndices(PySliceObject *self, PyObject *length,
def _PySlice_GetLongIndices(objSlice, intLength): 
    ''' 
    Compute slice indices given a slice and length.  
    Assumes that intLength is a nonnegative integer 
    '''
    start=None; stop=None; step=None
    upper=None; lower=None
    
    # Convert step to an integer; raise for zero step.
    if (objSlice.step is None):
        step = 1
        step_is_negative = False
    else:  
        step = evaluate_slice_index(objSlice.step)
        if ( step == 0 ): 
            raise ValueError( "slice step cannot be zero" )
        if step < 0:             
            step_is_negative = True
        else: 
            step_is_negative = False

    # Find lower and upper bounds for start and stop. 
    if (step_is_negative): 
        lower = -1
        upper = intLength + lower
    else: 
        lower = 0
        upper = intLength
    # ^-- this is the 'trick' to cope with the stop value for range() 
    #     providing values not including the stop value. 
    
    # Compute start: 
    if (objSlice.start == None):
        start = upper if step_is_negative else lower
    else: 
        start = evaluate_slice_index(objSlice.start)
        if ( start < 0):
            start = start + intLength
            if  start < lower: 
                start = lower
                #     ^-- explains how it comes, that any values 
                # for slice indices are OK. 
        else:
            if  start > upper: 
                start = upper
                #     ^-- explains how it comes, that any values 
                # for slice indices are OK. 
    # ^-- this is the 'trick' to get start from deliberate value
    #     into the range within the scope of valid list indices. 
    #     The positive/negative step value case is already handled
    #     by the choice of values for lower and upper. 
    
    # Compute stop: 
    if (objSlice.stop == None):
        stop = lower if step_is_negative else upper
    else: 
        stop = evaluate_slice_index(objSlice.stop);
        if ( stop < 0): 
            stop = stop + intLength
            if (stop < lower): 
                stop = lower;
        else:
            if (stop > upper):
                stop = upper;
    # ^-- this is the 'trick' to get stop from deliberate value
    #     into the range within the scope of valid stop indices. 
    #     The positive/negative step value case is already handled
    #     by the choice of values for lower and upper. 

    return (start, stop, step) # for range(start,stop,step) which can
    # be used to obtain the slice from a list using:
    #    [ theList[index] for index in range(start, stop, step ]  
#:def 

# Let's check if the code above does the same as the .indices() function
# of the slice object:
listL = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
for start in list(range(-3*len(listL), 3*len(listL))) + [None]: 
    for stop in list(range(-3*len(listL), 3*len(listL))) + [None]: 
        for step in list(range(-2*len(listL), 2*len(listL))) + [None]: 
            objSlice = slice(start,stop,step)
            try: 
                py = objSlice.indices(intLength)
            except:
                try: 
                    Py = _PySlice_GetLongIndices(objSlice, intLength)
                    echo(" STOP: difference in Exceptions")
                    import sys; sys.exit()
                except:
                    continue
            Py = _PySlice_GetLongIndices(objSlice, intLength)
            assert py == Py

listL = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
intLength = len(listL)

# If you ever wondered how to get the first element of a list with 
# a negative step, here two options how it can be done: 
assert listL[3:   2:-1] == [3]
assert listL[3:   1:-1] == [3, 2]
assert listL[3:   0:-1] == [3, 2, 1]
...
assert listL[3: -11:-1] == [3, 2, 1, 0]
assert listL[3:None:-1] == [3, 2, 1, 0]
assert listL[3::-1]     == [3, 2, 1, 0]
assert listL[3:-321:-1] == [3, 2, 1, 0]
# Both [:] and [::] have the same effect: 
assert listL[:]              == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
assert listL[::]             == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
objSlice = slice(None,None,None) # equivalent to [::] and [:]
assert listL[objSlice]       == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# also all the over variants lead to same result: 
assert listL[None:]          == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
assert listL[None:None]      == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
assert listL[None:None:None] == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

assert [ listL[indx] for indx in range(2,1,1) ] == []
# ^-- Another 'trick' of slicing is to start with an empty list.
#     If range(start,stop,step) don't deliver any value, an empty 
#     list without any added list elements is returned as result of
#     slicing. Exceptions are raised only on zero step value and
#     inappropriate types for start or stop values of the slice.  


The answers/resolutions are collected from stackoverflow, are licensed under cc by-sa 2.5 , cc by-sa 3.0 and cc by-sa 4.0 .