Python Simplified

Understanding Indexing and Slicing in Python

Indexing and Slicing

Introduction

We covered indexing and slicing briefly in one of the previous articles sequence types in Python. It seems like a very simple concept and there is no need for a separate article for that. But trust me, it deserves a separate article. Even if you know how to use indexing and slicing, I still suggest you go through the article for a thorough understanding of the subject so you don’t have to revise about indexing and slicing ever again. 

Indexing and slicing are only applicable to sequence data types. In sequence type, the order in which elements are inserted is maintained and this allows us to access its elements using indexing and slicing. 

To recap, the sequence types in Python are list, tuple, string, range, byte, and byte arrays. And indexing and slicing apply to all these types. 

Indexing

Indexing is used to access individual elements from the sequence. The general syntax of indexing is shown below — 

				
					seq[i]
				
			

where i is the index position. Here i can take both positive value (positive indexing) and negative value (negative indexing).

python-indexing
How to indexing works

Let’s look at some code below. Indexing starts from zero in Python. The elements in the 0th and 4th positions are 1 and 5. But when trying to access the elements outside of the range, you will run into an out-of-range error. In the below example, my_list[20] doesn’t exist. Hence you will run into IndexError.

				
					my_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
print(my_list[0])
print(my_list[4])
print(my_list[20])
				
			
				
					1
5
--------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-55-a470e93e26c2> in <module>
      2 print(my_list[0])
      3 print(my_list[4])
----> 4 print(my_list[20])

IndexError: list index out of range
				
			

You can also use negative indexing to access the elements in the sequence. Meaning you can reference the elements in the sequence from right to left.

In the below example, the elements in –5th and -10th positions are 60 and 10. With negative indexing also, you will run into an out-of-range error if you are trying to access the elements that are out of range. In the below example, my_list[-15] doesn’t exist. Hence you run into IndexError.

				
					print(my_list[-5])
print(my_list[-10])
print(my_list[-15])
				
			
				
					6
1
--------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-56-a291c133cf8d> in <module>
      2 print(my_list[-5])
      3 print(my_list[-10])
----> 4 print(my_list[-15])

IndexError: list index out of range
				
			

As you just saw indexing is pretty simple to understand. The key takeaway:
a) indexing is used to access individual elements from the sequence 
b) supports negative indexing 
c) out-of-range indices raise IndexError.

Slicing

Slicing is used to access one or more elements from the given sequence. The general syntax is as shown below:

				
					seq[i: j: k]
				
			
  • i = the starting index of the slice and it includes the element at this index.
  • j = the stop index of the slice but it doesn’t include the element at this index. 
  • k = step size. It indicates the amount by which the index increases. Defaults to 1. A negative value of k means slicing in reverse order (right to left).

The instruction seq[i: j: k] can be read as access all the elements from index position i till j (but not including element at j) with a step size of k.

python slicing ex1
How slicing works (example 1)
python slicing ex2
How slicing works (example 2)

In the below code, my_list[0:8:2], the start index is 0, the stop index is 8, and a step size 2. This translates into extract all the elements from index position 0 to 8 (excluding element at index 8) with a step size of 2. Here is the simplest example using slicing. We will get into more examples shortly.

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

slice()

The above example made use of the common approach of slicing using the square brackets [] and colon. There is also another way to do slicing using the built-in slice() function. Refer to the example below for the same. This achieves the same result as my_list[0:8:2]. Then, what is the use of the slice() function?

Refer to the example below for the same. This achieves the same result as my_list[0:8:2].

				
					my_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
s_obj = slice(0,8,2)
my_list[s_obj]
				
			
				
					Output
------
[1,3,5,7]
				
			

If both slicing notation and slice() function both give same results, then, what is the use of the slice() function?

Advantages of the slice() over slicing with square brackets

1) You can create the slice object only once in the program and use that slice object s_obj as many times as you want in the program. This avoids using slice notation (square brackets & colon) multiple times in the program. You could just use my_list(s_obj). If there is a need to change the slice values, you can change only once where it is defined.

2) Slice object has 3 methods using which we can access start index, stop index and step size as you can see below. This comes handy when you need to access the start index, stop index and step size in the program.

				
					my_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
s_obj = slice(0,8,2)

print(s_obj.start)
print(s_obj.stop)
print(s_obj.step)
				
			
				
					Output
------
0
8
2
				
			

3) This is another use of slice() function. The slice() function is called by Python internally when slicing notation is used. The my_list[0:8:2] is internally translated into my_list.__getitem__(slice(0,8,2)). You will learn more about __getitem__() in the next sections.

How slicing works in Python

Now we have a basic knowledge of indexing and slicing, it’s time to explore in detail. Slicing also supports negative indexing, you can omit or you can pass None to start index, stop index, and step size.

Slicing seems confusing at first. But don’t worry. We have got you covered. Once you go through the rest of this article, you would have mastered slicing in Python. 

Range equivalence

Any slice operation has an equivalent range function. This will be useful when you are confused about what all elements the slicing is going to select. It gives you the index values which then used to select the elements from the sequence.

For example, consider the list s = [10,20,30,40,50]. The result of s[::2] is [10, 30, 40]. As per the below transformation table, the equivalent range becomes range(0,5,2). Meaning select all the elements from index values 0, 2, 4 which is nothing but [10,30,50]. Hope you get the point. 

				
					>>> my_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> list(range(0, 5, 2))
# [0, 2, 4]
				
			

Slicing transformation rules

Refer to the below diagram carefully. This shows different transformations Python applies during slicing. Let’s look into examples showing how slicing works including the transformation rules mentioned in this table.

slicing rules

The value of k (step size) decides if we are processing the elements from left to right and right to left. So, we will split examples into 2 types for better understanding: when k > 0 and when k < 0. 

When step size (k) > 0

When step size (k) > 0, you are selecting the elements from left to right. Note that the output of the range() gives index positions. Then you need to select the elements at these index positions.

				
					>>> my_list = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]

# Example 1
>>> my_list[2:6] 
# The equivalent range is range(2,6). This means elements from index positions 2,3,4,5"""
[30, 40, 50, 60]

# Example 2
>>> my_list[1: 9: 2]
# The equivalent range is range(1,9,2). # This means elements from index positions 1,3,5,7'''
[20, 40, 60, 80]

# Example 3
>>> my_list[:]  
# The equivalent range is range(0,10). This means elemenets from index positions 0,1,2,3,4,5,6,7,8,9'''
>>> [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]

# Example 4
>>> my_list[: None] # Elements from start to end 
# The equivalent range is range(0,10). This means elements from index positions 0,1,2,3,4,5,6,7,8,9
[10, 20, 30, 40, 50, 60, 70, 80, 90, 100]

# Example 5
>>> my_list[1: -2: 2] 
# The equivalent range is range(1,8,2). This means elements from index positions 1,3,5,7.
[20, 40, 60, 80]

# Example 6
>>> my_list[-10: -1: 2] 
# The equivalent range is range(0,9,2). This means elements from index positions 0,2,4,6,8.
[10, 30, 50, 70, 90]

# Example 7
>>> my_list[-100: 200: 1]
# The equivalent range is range(0,10,1). 
[10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
				
			

When step size (k) < 0

When step size (k) > 0, you are selecting the elements from right to left (reverse direction).

				
					>>> my_list = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]

# Example 8
>>> my_list[::-1] 
# The equivalent range is range(9,-1,-1). This means elements from index positions 9,8,7,6,5,4,3,2,1,0"""
[100, 90, 80, 70, 60, 50, 40, 30, 20, 10]

# Example 9
>>> my_list[-1: -7: -2]
# The equivalent range is range(9,3,-2). This means elements from index positions 9,7,5
[100, 80, 60]
				
			

Why slicing doesn't raise an out-of-range error

You have seen that indexing raises an out-of-range error when trying to access out of bound elements in a sequence. But slicing doesn’t raise an out-of-range error. Because, as you have seen in the previous examples, whenever i & j are out-of-bound Python applies transformation based on the length of the sequence. So, slicing out-of-range is handled gracefully by Python.

What happens under the hood when you use slicing/indexing

When you use slicing/indexing using square brackets [], Python actually calls the dunder method __getitem__() under the hood. Let’s look at some examples. 

As you can see, my_list[0] is same as my_list.__getitem__(0). The same holds true for indexing any sequence types.

				
					>>> my_list = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]

# Examples using indexing
>>> my_list.__getitem__(0)
10

>>> my_list.__getitem__(-1)
100

>>> my_list.__getitem__(-2)
90
				
			
				
					# Example using slicing
>>> my_list.__getitem__(slice(0, 11, 2))
[10, 30, 50, 70, 90]

>>> my_list.__getitem__(slice(-1, -11, -1))
[100, 90, 80, 70, 60, 50, 40, 30, 20, 10]
				
			

There is also __setitem__() method that can be used to set the values using indexing and slicing. 

				
					# Example 1
>>> my_list = [10, 20, 30, 40, 50]
>>> my_list[0] = 1  #same as my_list.__setitem__(0, 1)
>>> my_list
[1, 20, 30, 40, 50]

# Example 2
>>> my_list = [10, 20, 30, 40, 50]
>>> my_list.__setitem__(0, 11)
>>> my_list
[11, 20, 30, 40, 50]

# Example 3
>>> my_list.__setitem__(slice(0,5), [1,2,3,4,5])
>>> my_list
[1, 2, 3, 4, 5]
				
			

Summary

  • Indexing is used to access individual elements from the sequence. And it supports both positive and negative indexing.
  • Indexing raise IndexError for out-of-range indices.
  • Slicing has an equivalent range function that will help you to understand which will be selected during the slicing operation.
  • Slicing handles out-of-range indices so you won’t run into IndexError with slicing.
  • Under the hood, indexing and slicing calls __getitem__(). And during __setitem__() will be called when setting the values using indexing and slicing.

References

Share on facebook
Share on twitter
Share on linkedin
Share on whatsapp
Share on email
Chetan Ambi

Chetan Ambi

A Software Engineer & Team Lead with over 10+ years of IT experience, a Technical Blogger with a passion for cutting edge technology. Currently working in the field of Python, Machine Learning & Data Science. Chetan Ambi holds a Bachelor of Engineering Degree in Computer Science.
Scroll to Top