Understanding Python slices

Let’s say that you have a Python string, and want to grab a substring from it. The best way to do so is with a “slice”:

>>> s = 'abcdefghij'
>>> print(s[3:8])
defgh

In the above code, we’ve defined a string. We’ve then asked for the string to be returned, starting at index 3 and up to (and not including) index 8. Sure enough, we get the string ‘defgh’ back.

Slices work on all sequences in Python. Which means that you can have a slice of a string, list, or tuple, and you’ll have the same effect:

>>> mylist = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
>>> mylist[3:7]
[40, 50, 60, 70]

While “for” loops in Python don’t explicitly use numeric indexes, slices do. My students often ask me how, without indexes, they can iterate over only part of a string (or other sequence). The answer is to use a slice. For example, if I’m interested in iterating over letters 3-7 in my string “s” from above, I won’t (and can’t) use a C-like “for” loop, starting the loop at index 3 and ending at index 7. Rather, I need to run my “for” loop over a complete string… which a slice just happens to return to me:

for one_letter in s[3:7]:   # s[3:7] returns a new string
print(one_letter)

Remember that a slice from type X will always return a new object of type X. Thus, slicing a string returns a string, while slicing a list returns a list and slicing a tuple returns a tuple. For example:

>>> mylist[3]           # one index
40
>>> mylist[3:7] # slice
[40, 50, 60, 70]
>>> mylist[3] * 2 # Multiply the element at mylist[3] by 2
80
>>> mylist[3:7] * 2 Multiply the slice (list) at mylist[3:7] by 2
[40, 50, 60, 70, 40, 50, 60, 70]

Slice syntax is more flexible than this: You can leave off the starting index, ending index, or both to indicate that you want to go all the way to the extreme edge:

>>> s
'abcdefghij'
>>> s[:5] # from the start until (and not including) index 5
'abcde'
>>> s[5:] # from index 5 until (and through) the end
'fghij'
>>> s[5:-1] # not the same as s[5:] -- doesn't include the end
'fghi'

Because slices create new objects, you can sometimes use them to avoid problems with mutable data:

>>> mylist = [10, 20, 30]
>>> biglist = [mylist, mylist]
>>> mylist[0] = '!'
>>> biglist
[['!', 20, 30], ['!', 20, 30]]

In the above code, changing “mylist” affected “biglist”, because both names were still pointing (one directly, one indirectly) to the original list. By contrast, if I use slices, I can avoid this:

>>> mylist = [10, 20, 30]
>>> biglist = [mylist[:], mylist[:]] # put copies, not mylist, in biglist
>>> mylist[0] = '!'
>>> biglist
[[10, 20, 30], [10, 20, 30]

Note that this isn’t a perfect solution; if you’re copying complex data structures, then you’ll probably want to look at the “copy” module, and explore its “copy” and “deepcopy” methods.

By default, a slice uses each element from the sequence on which it’s working. But what if we only want every other element? Then we can use the (optional) third part of slice syntax, known as either the “step size” or the “stride.” By default, the step size is 1. But by adding another number after a second colon, we can modify this, too:

>>> mylist = [10, 20, 30, 40, 50, 60, 70, 80]
>>> mylist[2:7] # step size of 1 (default)
[30, 40, 50, 60, 70]
mylist[2:7:2] # step size of 2
[30, 50, 70]

With the combination of start-stop-stride, we can work with all sorts of combinations of things:

>>> import string
>>> s = string.ascii_lowercase
>>> s[5:20:3] # from 5, until (not including) 20, step size 3
'filor'
>>> s[2:25:4] # from 2, until (not including) 25, step size 4
'cgkosw'
>>> s[:25:4] # from start, until (not including) 25, step size 4
'aeimquy'
>>> s[2::4] # from 2, through the end, step size 4
'cgkosw'
>>> s[2::2] # from 2, through the end, step size 2
'cegikmoqsuwy

It gets even better when you discover that the step size can be negative, which allows us to retrieve values in reverse order from the original data structure. Just remember, in such cases, that the start needs to be bigger than the end:

>>> s[20:5:-1]        # from 20 to 5 (not including), step size 1
'utsrqponmlkjihg'
>>> s[20:5:-4] # from 20 to 5 (not including), step size 4
'uqmi'
>>> s[:5:-4] # from the end to 5 (not including), step size 4
'zvrnj'
>>> s[20::-2] # from 20 to the beginning, step size 2
'usqomkigeca'
>>> s[::-1] # no indexes? from end to start, backwards
'zyxwvutsrqponmlkjihgfedcba'
>>> s[5:20:-5] # Start is smaller, and negative step? Bad news...
''

Just as you can use variables in an index, you can also use them in a slice:

>>> s = 'abcdefghij'
>>> i = 3
>>> j = 6
>>> s[i]
'd'
>>> s[i:j]
'def'
>>> s[::i]
'adgj'

Normally, if you try to retrieve from an index that’s beyond the bounds of the data structure, you’ll get an error:

>>> s = 'abcdefghij'
>>> s[15]
IndexError: string index out of range

But slices are far more forgiving; if you go off of the edge of a slice, then Python will simply stop at the edge of your sequence:

>>> s = 'abcdefghij'
>>> s[:15]
'abcdefghij'

You can also assign to slices. For the most part, this means modifying multiple elements in a list:

>>> mylist = [10, 20, 30, 40, 50, 60, 70]
>>> mylist[3:5]
[40, 50]
>>> mylist[3:5] = 'XY'
>>> mylist
[10, 20, 30, 'X', 'Y', 60, 70]

Notice, in the above example, that we assigned to a slice from a string. So long as the item on the right side is iterable, you can assign it to a slice.

You can expand and contract a list by assigning more or fewer item to a slice:

>>> mylist = [10, 20, 30, 40, 50, 60, 70]
>>> mylist[3:5] = 'WXYZ'
>>> mylist
[10, 20, 30, 'W', 'X', 'Y', 'Z', 60, 70]

In the above example, our slice was only two items long. However, we assigned four elements to it. This meant that the list grew as a result of our assignment. We can similarly shrink it:

>>> mylist
[10, 20, 30, 'W', 'X', 'Y', 'Z', 60, 70]
>>> mylist[3:7] = [99, 98]
>>> mylist
[10, 20, 30, 99, 98, 60, 70]

While we normally think about only sequences as being sliceable, other objects can potentially work with slices, too. For example, the “range” object in Python 3 is sliceable:

>>> r = range(100, 300, 3)
>>> r[4:8]
range(112, 124, 3)
>>> r[10:20:2]
range(130, 160, 6)

Notice that the printed representation of a range object includes the three parts that we’ve discussed in a slice: The start, the end (+1), and the step size.

And indeed, Python provides a “slice” builtin that I’ve never used, but which I can imagine would be useful if you want to reuse a slice multiple times:

>>> s = 'abcdefghij'
>>> myslice = slice(2, 8, 2)
>>> s[myslice]
'ceg'
>>> s[2:8:2]
'ceg'

>>> myslice = slice(None, None, -2)
>>> s[myslice]
'jhfdb'
>>> s[::-2]
'jhfdb'

Notice how our “slice” object works just like the start:end:step syntax; if you want to indicate the edge (via nothing between the colons), then you can use “None”. Again, I’m not really sure why you would need a slice object, but it’s nice to know that everything in Python is indeed an object, and that the :: syntax is translated into a slice object in the end.

What if you want your own objects to be sliceable? Truth be told, there’s not much to do: The __getitem__ method is used for retrieving individual items as well as slices; while there used to be a __getslice__ method, nowadays you are expected to write __getitem__ such that it handles individual indexes and slices. In many cases, that’s trivially easy to do:

class Foo():
def init(self, x):
self.x = x
def getitem(self, index):
return self.x[index]

>>> f = Foo('abcdefghij')
>>> f[3]
'd'
>> f[3:5]
'de'

Of course, if you want to do something more sophisticated than returning one or many elements from your object, then you’ll have to work a bit harder. But with an if/else, you can make such decisions, and then return the appropriate data.

Slices are both common and convenient ways to extract portions of Python data structures — usually with builtin objects, but also on your own. Once you get used to slices, you’ll see lots of uses for them, and wonder how you got along without them.

Click Here to Leave a Comment Below 0 comments

Leave a Reply: