The world’s leading publication for data science, AI, and ML professionals.

Four Things You Should Know in Python

Learn how to slice sequences, loop through sequences, zip/unzip iterables, and more in Python

With the vast amount of Python resources available, it can be extremely difficult to know which available tools are actually useful to know.

In this article, we will focus on four tools that, in my opinion, are incredibly valuable to know in Python. We will first discuss how to slice sequences in Python, specifically lists and strings, including how to easily reverse them. Then we will move on to one of the best ways to loop through a sequence, keeping track of both the index and element, by using the enumerate() function. After that, we will learn about the zip() function, which can be used to zip and unzip iterable objects. Lastly, we will review the best way to format strings using f-strings.


1. Slicing Sequences

Slicing allows us to obtain a portion from a sequence, such as of a list or string.

variable[start:stop:step]

In order to slice a sequence, we need to use a colon within square brackets. In other words, the colon (:) in subscript notation [square brackets] make slice notation. Even though there are three possible values that we can provide within the brackets (the start value, stop value, and step/stride value), we don’t actually have to provide all three unless we need to, as we will see below.

Let’s look at some examples.


Specifying Start and Stop Values

One way we can slice a sequence, such as a list, is by specifying the start and stop values. In other words, if we want all the elements between two specific points in our list, we can use the following format:

variable[start:stop]

variable[start:stop] returns the portion of the variable that starts with position start, and up to but not including position stop.

For example, if we want to obtain all the elements from index 2 up to and including index 6, we can do so as follows:

num_list = [0,5,10,15,20,25,30,35,40]
num_list[2:7]
#[10,15,20,25,30]

Notice how the start value is inclusive, but the stop value is exclusive. Since we did not provide a step value, the default step value is 1. Thus, we start with index 2, then take 1 step to index 3, then 1 step to index 4, and so on. In other words, because the step value is positive, we are increasing the index by 1 (moving to the right) as we are slicing our list.


Specifying Only a Start Value

If we want to start at a specific number and go through the entire list, then we would only need to provide the start value.

variable[start:]

variable[start:] returns the portion of the variable that starts with position start, through the end of the sequence.

For example, if we want to retrieve all the elements from the second index through the entire list, we can use the following code:

num_list = [0,5,10,15,20,25,30,35,40]
num_list[2:]
#[10,15,20,25,30,35,40]

As we can see, if we only provide an index before the colon, then that will be our start index, and we will obtain the rest of the elements in the list (since the step value is still 1).


Specifying Only a Stop Value

If we want to start from the beginning of the list and go up to a specific index, then we would only need to provide the stop value.

variable[:stop]

variable[:stop] returns the portion of the variable that starts at the beginning of the sequence, up to but not including position stop.

For example, if we want to retrieve all the elements from the start of the list up to and including index 7, we can do so as follows:

num_list = [0,5,10,15,20,25,30,35,40]
num_list[:8]
or
num_list[:-1]
#[0,5,10,15,20,25,30,35]

If no number is provided for the start value, then it assumes that we want to start from index 0. And since we want to retrieve all elements up until index 7, we would use the stop value of 8 since it is exclusive. We can also use -1 as the stop value.


Using Positive and Negative Indices

We can also mix and match the positive and negative indices. For example, if we want to retrieve all the elements between index 2 up to and including index 7, we can do so as follows:

num_list = [0,5,10,15,20,25,30,35,40]
num_list[2:8]
or 
num_list[2:-1]
or
num_list[-7:-1]
or
num_list[-7:8]
#[10,15,20,25,30,35]

Note that in all instances, the stop value is to the right of the start value, since we are using a positive step value. In other words, the stop value must be in the direction of the step value, in relation to the start value. If the step value is positive, then the stop value must be right of the start value. If the step value is negative, then the stop value must be left of the start value. More on that later.


Retrieving the Entire List

We can also retrieve the entire list by just using a colon with no start or stop values.

variable[:]

variable[:] returns the entire sequence.

num_list = [0,5,10,15,20,25,30,35,40]
num_list[:]
or 
num_list[::]
#[0,5,10,15,20,25,30,35,40]

Step (or Stride) Value

So far we’ve only specified start and/or stop values, where we start at the start value, and end right before the stop value (since it is exclusive). But what if we don’t want all the elements between those two points? What if we want every other element? That’s where the step or stride value comes in.

Let’s say that we want every other value in our list, starting from index 0. Or perhaps we only want the elements at even indices. We can do so by changing the step value:

variable[::step]

num_list = [0,5,10,15,20,25,30,35,40]
num_list[::2]
#[0,10,20,30,40]

Since we did not specify a start or stop value, it assumes that we want to start at the beginning of the sequence and go through the entire list. So it starts at index 0, then goes to index 2 (since the step is 2), then to index 4, and so on.

Previously, we mentioned that the stop value must be in the same direction as the step value relative to the start value. In other words, if the step value is positive, which means we are moving to the right, the stop value must be to the right of the start value. If the step value is negative, then the stop value must be to the left of the start value. Otherwise, an empty list is returned:

num_list = [0,5,10,15,20,25,30,35,40]
num_list[8:5]
#[]
num_list[8:5:-1]
#[40,35,30]

As we can see, in both examples, the start value is 8, and the stop value is 5, so the stop value is to the left of the start value. In the first example, the step value is +1. Since the stop value is to the left of the start value, and our step value is positive, an empty list is returned, since we cannot move in the direction of the stop value. However, in the second example, we changed the step value to -1. Thus, we start at index 8, which is 40, move 1 index in the negative or left direction to index 7, which is 35, then to index 6, which is 30. We do not go to index 5 because the stop value is exclusive.


Reversing a Sequence

Perhaps the most important practical application of the step value is to reverse a sequence. For example, if we want to retrieve the entire list in reverse order, we can do so by using a -1 for the step value:

num_list[::-1]
#[40,35,30,25,20,15,10,5,0]

Since we did not specify a start or stop value, then the entire sequence will be retrieved. However, since our step value is -1, it obtains the elements in reverse order.


What if our stop value is greater than the highest index available in our sequence? Or if our start and/or stop values are out of range? In other words, what happens if we ask for more items than are present?

For example, if we try the following:

num_list = [0,5,10,15,20,25,30,35,40]
num_list[2:12]
#[10,15,20,25,30,35,40]

As we can see, even if we ask for more items than present in our sequence, it just returns whatever elements are present and does not give us an error. In contrast, if we try to index a single element that is out of range (instead of slicing), then we would get an IndexError as we saw earlier.

num_list[12]
#IndexError

Slicing Strings

Indexing and slicing work the same way for strings as well.

word = 'Python'

Thus, to obtain the substring ‘yt’ via slicing, we can do so as follows:

word[1:3]
#'yt'

To reverse a string, we can do so by using a step value of -1:

word[::-1]
#'nohtyP'

Palindrome Example

Let’s use what we learned to solve a very commonly asked python coding question. We want to write a function that takes in a string, and returns whether or not that string is a palindrome. A string is a palindrome if the reverse of the string is identical to the original string. For example, ‘civic’ is a palindrome, but ‘radio’ is not, since the reverse of ‘radio’ is ‘oidar’, but the reverse of ‘civic’ is ‘civic’.

We just learned how to reverse a sequence by using a step value of -1. Thus we can easily write a function that accomplishes this as follows:

isPalindrome(word):
    return word == word[::-1]

And that’s it! The expression word == word[::-1] is evaluated to either True or False. If the string we pass in is equal to its reverse, then the expression evaluates to True, and True is returned. If the string we pass in does not equal its reverse, then the expression evaluates to False, and False is returned.

isPalindrome('civic')
# True
isPalindrome('radio')
# False

Slice Assignment

If we recall, lists are mutable objects in python. In other words, they are able to be mutated, or changed. Thus, we can use slice assignment operation to mutate or edit a list in place.


Substitution

num_list = [0,5,10,15,20,25,30,35,40]
num_list[2:5] = [1,2,3]
num_list
#[0,5,1,2,3,25,30,35,40]
num_list[2:5] = [1,2,3,4,5,6]
num_list
#[0,5,1,2,3,4,5,6,25,30,35,40]

Notice how we can replace a slice of our list with more or less elements.


Deletion

We can also delete a part or slice of a list using the del keyword:

num_list = [0,5,10,15,20,25,30,35,40]
del num_list[2:5]
num_list
#[0,5,25,30,35,40]

Strings and tuples are not mutable. Thus we can not edit or mutate them like we can with lists.


Slicing Strings vs. Lists

Slicing a list will return a copy of that list and not a reference to the original list.

We can see this here: if we assign our list slice to another list, since the list slice returns a copy and not a reference to the original list, we can modify the new list (since lists are mutable) without affecting the original list:

num_list = [0,5,10,15,20,25,30,35,40]
# assign a slice of num_list to new_list
new_list = num_list[2:5]
new_list
#[10,15,20]
# replace the third element of new_list with 3
new_list[2] = 3
# new_list changes
new_list
#[10,15,3]
# num_list remains the same
num_list
#[0,5,10,15,20,25,30,35,40]

In contrast, when we slice a string, a reference to the original string object is returned, and not a copy. And remember, strings are not mutable in Python.

We can use Python’s identity operator (is) and the equality operator (==) to confirm that slicing a list returns a copy or a different object than the original list, but slicing a string returns a reference to the original string object:

Lists:
num_list = [0,5,10,15,20,25,30,35,40]
num_list == num_list[:]
#True
num_list is num_list[:]
#False
Strings:
word = 'Python'
word == word[:]
#True
word is word[:]
#True

The equality operator (==) checks if the values are equal. The identity operator (is) checks if the two variables point to the same object in memory.


For more information on slicing sequences:

How to Slice Sequences in Python


2. enumerate() Function

enumerate() allows us to iterate through a sequence but it keeps track of both the element and the index.

enumerate(iterable, start=0)

The enumerate() function takes in an iterable as an argument, such as a list, string, tuple, or dictionary. In addition, it can also take in an optional argument, start, which specifies the number we want the index to start at (the default is 0).

Using the enumerate() function, we can write a for loop :

num_list= [42, 56, 39, 59, 99]
for index, element in enumerate(num_list):
    print(index, element)
# output: 
0 42
1 56
2 39
3 59
4 99

The enumerate() function returns an enumerate object, which is an iterator. As each element is accessed from this enumerate object, a tuple is returned, containing the index and element at that index: (index, element). Thus, in the above for loop, with each iteration, it is assigning the elements of this returned tuple to the index and element variables. In other words, the returned tuple is being unpacked inside the for-statement.


We can also specify starting the index at 1 by passing in 1 as the argument for the start parameter of the enumerate() function:

num_list = [42, 56, 39, 59, 99]
for index, element in enumerate(num_list, start=1):
    print(index, element)
# output: 
1 42
2 56
3 39
4 59
5 99

For more on the enumerate() function and iterators:

Looping in Python

Iterables and Iterators in Python


3. zip() Function

The zip() function is named due to its analogous mechanism as physical zippers. When you zip something, you bring both sides together. And that’s how the zip() function works! It brings elements of the same index from multiple iterable objects, such as lists, together as elements of the same tuples.

zip(*iterables)

For example, let’s say we have two lists, _first_names and last_names_, and we want to combine elements of the same index from both lists into a list of tuples as follows:

first_names = ['Jane', 'John', 'Jennifer']
last_names = ['Doe', 'Williams', 'Smith']
Desired Output:
[('Jane', 'Doe'), ('John', 'Williams'), ('Jennifer', 'Smith')]

We can use python’s built-in zip() function to accomplish this:

first_names = ['Jane', 'John', 'Jennifer']
last_names = ['Doe', 'Williams', 'Smith']
full_names = list(zip(first_names, last_names))
print(full_names)
# [('Jane', 'Doe'), ('John', 'Williams'), ('Jennifer', 'Smith')]

The zip() function takes in iterables as arguments, such as lists, files, tuples, sets, etc. The zip() function will then create an iterator that aggregates elements from each of the iterables passed in. In other words, it will return an iterator of tuples, where the i-th tuple will contain the i-th element from each of the iterables passed in. This iterator will stop once the shortest input iterable has been exhausted.

Note: Since the zip() function returns an iterator, we need to use the list() function that will use this returned iterator (or zip object) to create a list. In addition, as long as the iterables passed in are ordered (sequences), then the tuples will contain elements in the same left-to-right order of the arguments passed in the zip() function.


Using the zip() Function to Unzip IterablesIn contrast to zipping, which brings things together, unzipping takes things apart. We can also use the zip() function to unzip iterable objects in python by adding the unpacking operator * as follows:

first_and_last_names = [('Jane', 'Doe'), ('John', 'Williams'), ('Jennifer', 'Smith')]
first_names, last_names = zip(*first_and_last_names)
first_names = list(first_names)
last_names = list(last_names)
print(first_names)
# ['Jane', 'John', 'Jennifer']
print(last_names)
# ['Doe', 'Williams', 'Smith']

The **unpacking operator * will unpack the first_and_last_names __ list of tuples into its tuples. These tuples will then be passed to the zip() function, which will take these separate iterable objects (the tuples), and combines their same-indexed elements together into tuples, making two separate tuples. Lastly, through tuple unpacking, these separated tuples will be assigned to the first_names and last_names variables. We then use the list(**) function to convert these tuples into lists.


For more on the zip() function and unpacking operators:

Zipping and Unzipping Iterables in Python

Unpacking Operators in Python


4. f-strings

F-strings are a new way to format strings in Python 3.6 and above. For most people, they are the preferred way to format strings since they are easy to read and much more intuitive.


We will use the _first_name, last_name, and age_ variables to create a string that contains the first name, last name, and age of someone in the following format:

_’first_name lastname is age years old’


To specify that we want to use an f-string, or formatted string, we just put an f in front of the string. Then we can directly add our variables into our curly brackets in the location we want them to appear. Thus, we didn’t need to use any method or pass in any variables at the end of the string like we would with the format() method.

f'{var_1} {var_2} is {var_3} years old’

Thus, to accomplish the above task with f-strings, we would use the following code:

first_name = 'John'
last_name = 'Doe'
age = 43
sentence = f'{first_name} {last_name} is {age} years old'
print(sentence)
# 'John Doe is 43 years old'

Similar to the format() method, we can also run methods or functions within the f-string. For example, if we want to make the first name uppercase and last name lowercase, we can do so with the corresponding string methods within the f-string:

first_name = 'John'
last_name = 'Doe'
age = 43
sentence = f'{first_name.upper()} {last_name.lower()} is {age} years old'
print(sentence)
# 'JOHN doe is 43 years old'

If you enjoy reading stories like these and want to support me as a writer, consider signing up to become a Medium member. It’s $5 a month, giving you unlimited access to stories on Medium. If you sign up using my link, I’ll earn a small commission.

Join Medium with my referral link – Luay Matalka


Conclusion

In this tutorial, we looked at 4 extremely useful tools in Python. We first learned how to slice sequences, such as lists and strings, retrieving portions of those sequences by using square brackets and one or more colons. We also saw how slice assignment works and the differences between slicing a list and slicing a string in that slicing a list returns a copy of that list but slicing a string returns a reference to the original string object. Next, we looked at how to zip iterable objects using the zip() function, and unzip iterable objects using the zip() function with the unpacking operator *. Then, we learned how we can use the enumerate() function to keep track of both the element and the index as we loop over an iterable object. Lastly, we learned how to format strings by using the elegant and concise f-strings.


Related Articles