10  Data Structures: Lists and Tuples

In the previous chapters, we introduced the basic elements of programming in Python, including variables, data types, control structures, and functions. Now, we turn our attention to an essential topic in computing — data structures. Data structures allow us to organize and store data in ways that enable efficient access and modification. In this chapter, we will explore two foundational data structures in Python: Lists and Tuples.

10.1 Lists

In Python, lists are one of the most commonly used data structures due to their flexibility and ease of use. A list is a mutable, ordered collection of elements, which means the elements in a list can be changed after the list is created, and they are stored in a specific order. This makes lists ideal for storing sequences of data that might need to be altered during the execution of a program.

10.1.1 Creating Lists

Lists in Python are defined using square brackets ([]), with each element separated by a comma. A list can contain elements of any data type, including integers, floats, strings, Booleans, and even other lists.

Examples:

# A list of integers
integer_list = [1, 2, 3, 4, 5]

# A list of mixed data types
mixed_list = [42, "apple", 3.14, True]

# A list of lists
lists_list = [integer_list, mixed_list]

The flexibility of lists allows you to store a wide variety of data in a single collection, making them useful in many programming contexts, such as storing records, handling input, and building dynamic datasets.

10.1.2 Accessing Elements in a List

Each element in a list is associated with an index—an integer representing the element’s position in the list. In Python, indices start at 0, meaning the first element is accessed with index 0, the second element with index 1, and so on. Negative indices can also be used to access elements from the end of the list, with -1 referring to the last element, -2 to the second-to-last, and so on.

Examples:

# Accessing elements by positive index
my_list = [10, 20, 30, 40]
print(my_list[0])  # Output: 10
print(my_list[2])  # Output: 30

# Accessing elements by negative index
print(my_list[-1])  # Output: 40
print(my_list[-2])  # Output: 30
10
30
40
30

10.1.3 Modifying Lists

One of the most powerful features of lists is their mutability. This means that after a list is created, its elements can be changed, added, or removed without creating a new list. There are several ways to modify lists in Python.

Changing Elements

You can change individual elements in a list by assigning a new value to a specific index:

my_list = [1, 2, 3]
my_list[1] = 99  
print(my_list)  
[1, 99, 3]

Adding Elements

You can add elements to a list using the append() method (to add a single element) or the extend() method (to add multiple elements):

# Adding a single element
my_list = [1, 2, 3]
my_list.append(4)
print(my_list)

# Adding multiple elements
my_list.extend([5, 6])
print(my_list)
[1, 2, 3, 4]
[1, 2, 3, 4, 5, 6]

You can also use the insert() method to add an element at a specific index:

my_list = [1, 2, 3]
my_list.insert(1, "new")
print(my_list)  
[1, 'new', 2, 3]
Note

In Python, a method is a function that is associated with an object. Every method is a function, but not every function is a method. The key difference is that methods are called on objects, and they are designed to perform actions related to those objects. The syntax for calling a method involves the dot notation, where you first specify the object and then call the method.

Methods are tightly bound to the object they belong to and often manipulate or interact with the data stored in the object. Python has built-in methods for its standard data types like lists, strings, dictionaries, etc.

Removing Elements

Elements can be removed from a list using the remove() method (to remove the first occurrence of a specific element) or the pop() method (to remove an element by its index):

# Removing an element by value
my_list = [1, 2, 3, 2]
my_list.remove(2)
print(my_list)  

# Removing an element by index
my_list.pop(1) #note that this returns the value being removed
print(my_list)
[1, 3, 2]
[1, 2]

To clear all elements from a list, use the clear() method:

my_list = [1, 2, 3]
my_list.clear()
print(my_list)
[]

10.1.4 Slicing Lists

In addition to accessing individual elements, Python allows you to slice lists, which means extracting a portion of the list to create a new list. Slicing is done using the colon (:) operator, with the format list[start:end]. The start index is inclusive, while the end index is exclusive.

Examples:

my_list = [10, 20, 30, 40, 50]

# Extract elements from index 1 to 3 (2nd to 4th elements)
print(my_list[1:4])  # Output: [20, 30, 40]

# Extract elements from the start to index 3
print(my_list[:4])  # Output: [10, 20, 30, 40]

# Extract elements from index 2 to the end
print(my_list[2:])  # Output: [30, 40, 50]
[20, 30, 40]
[10, 20, 30, 40]
[30, 40, 50]

Slicing can also be used with a step value, which specifies how many elements to skip between items:

# Extract every other element
print(my_list[::2]) 
[10, 30, 50]

10.1.5 Common List Operations

Python provides several built-in functions and operators that can be applied to lists. Below are some of the most commonly used list operations:

  1. Checking Length: You can find the number of elements in a list using the len() function:

    my_list = [10, 20, 30]
    print(len(my_list))
    3
  2. Membership Testing: You can check whether an element is in a list using the in keyword:

    my_list = [10, 20, 30]
    print(20 in my_list) 
    True
  3. Sorting Lists: Lists can be sorted in place using the sort() method, or a sorted copy of the list can be returned using the sorted() function:

    my_list = [3, 1, 4, 1, 5, 9]
    my_list.sort()  # Sort the list in place
    print(my_list)
    [1, 1, 3, 4, 5, 9]
  4. Reversing a List: You can reverse the order of elements in a list using the reverse() method:

    my_list = [1, 2, 3]
    my_list.reverse()
    print(my_list) 
    [3, 2, 1]

10.1.6 Iterating Over Lists

Lists are iterable, which means you can loop through the elements using a for loop:

my_list = ["apple", "banana", "cherry"]
for fruit in my_list:
    print(fruit)
apple
banana
cherry

You can also iterate over both the index and the element using the enumerate() function:

my_list = ["apple", "banana", "cherry"]
for index, fruit in enumerate(my_list):
    print(f"Index {index}: {fruit}")

Nesting Lists

Lists can contain other lists as elements, allowing you to create more complex data structures like matrices or grids. This is known as nesting.

Example:

matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

# Accessing the second element of the first list
print(matrix[0][1])  # Output: 2

# Iterating over nested lists
for row in matrix:
    print(row)
2
[1, 2, 3]
[4, 5, 6]
[7, 8, 9]

Lists and Built-in Libraries

Lists can be used effectively with common Python libraries. For example, the random module allows for random selection of elements from a list:

Example:

import random

fruits = ["apple", "banana", "cherry", "date"]
print(random.choice(fruits))  # Randomly selects and prints one fruit
date

10.2 Tuples

While lists offer flexibility through their mutability, Python also provides tuples, which are similar in structure but immutable. Once a tuple is created, its elements cannot be changed. This immutability makes tuples useful for representing fixed collections of items that should not or cannot change throughout the execution of a program. Tuples are ideal when you need to ensure data integrity, such as coordinates, configuration settings, or when passing multiple values from functions where immutability is expected.

10.2.1 Creating Tuples

Tuples are defined using parentheses () and can hold elements of any data type, much like lists. However, since they are immutable, you cannot modify the elements of a tuple after it is created.

Syntax:

# Creating a tuple
my_tuple = (10, 20, 30)

It’s also possible to create tuples without using parentheses, though this is less common:

my_tuple = 10, 20, 30

Tuples can contain elements of different types:

mixed_tuple = (1, "apple", 3.14, True)

Tuples with a single element need to have a comma after the element to avoid confusion with parentheses used in expressions:

single_element_tuple = (5,)
print(type(single_element_tuple))
<class 'tuple'>

10.2.2 Accessing Tuple Elements

Like lists, tuples are indexed starting at 0, and individual elements can be accessed using their index. However, since tuples are immutable, you cannot modify their elements.

Examples:

my_tuple = (10, 20, 30, 40)

# Accessing the first element
print(my_tuple[0]) 

# Accessing the last element using negative indexing
print(my_tuple[-1]) 
10
40

You can slice tuples just like lists, returning a new tuple containing the specified range of elements:

# Slicing a tuple
print(my_tuple[1:3]) 
(20, 30)

10.2.3 Tuple Operations

Although tuples are immutable, there are still several operations you can perform on them:

  1. Length of a Tuple: You can find the number of elements in a tuple using the len() function:

    my_tuple = (10, 20, 30)
    print(len(my_tuple))  
    3
  2. Membership Testing: Use the in keyword to check whether an element exists in a tuple:

    my_tuple = (10, 20, 30)
    print(20 in my_tuple)  
    True
  3. Iterating Over a Tuple: Tuples are iterable, so you can loop through their elements using a for loop:

    for item in my_tuple:
        print(item)
    10
    20
    30
  4. Indexing and Slicing: Like lists, tuples support indexing and slicing:

    my_tuple = (10, 20, 30, 40)
    print(my_tuple[1:3])  
    (20, 30)

10.2.4 Nested Tuples

Tuples can be nested inside other tuples, allowing you to create multi-level data structures. This is useful when organizing complex data, such as in multidimensional arrays or coordinate systems.

Example:

nested_tuple = ((1, 2), (3, 4), (5, 6))

# Accessing the first tuple
print(nested_tuple[0])  

# Accessing an element from a nested tuple
print(nested_tuple[0][1])  
(1, 2)
2

10.2.5 Unpacking Tuples

One of the most powerful features of tuples is tuple unpacking, which allows you to assign the elements of a tuple to individual variables in a single statement.

Example:

# Unpacking a tuple into variables
coordinates = (10, 20)
x, y = coordinates
print(x)  
print(y)  
10
20

Tuple unpacking is especially useful when functions return multiple values in a tuple, allowing you to capture and work with these values directly.

Example:

def rectangle_properties(length, width):
    area = length * width
    perimeter = 2 * (length + width)
    return (area, perimeter)

# Unpacking the returned tuple into variables
area, perimeter = rectangle_properties(5, 10)
print(f"Area: {area}, Perimeter: {perimeter}")
Area: 50, Perimeter: 30

10.2.6 Tuple Methods

Since tuples are immutable, they have fewer methods compared to lists. However, they do provide two useful methods:

  1. count(): Returns the number of times a specified value appears in the tuple.

    my_tuple = (1, 2, 2, 3, 2)
    print(my_tuple.count(2))  
    3
  2. index(): Returns the index of the first occurrence of a specified value.

    my_tuple = (1, 2, 3)
    print(my_tuple.index(2)) 
    1

10.3 List Comprehension

In Python, list comprehension provides a concise way to generate lists. It offers a more readable and often more efficient alternative to using loops and append() to build lists. List comprehension allows you to apply an expression to each element in a sequence and optionally include conditional statements to filter elements. This compact syntax makes it easy to create lists based on existing sequences or from operations.

10.3.1 Basic Syntax of List Comprehension

The basic syntax of list comprehension is:

[expression for item in iterable]
  • expression: This is the operation or value you want to apply to each item in the iterable.
  • item: Each element from the iterable (e.g., a list, tuple, or string).
  • iterable: The sequence of elements to loop over (e.g., a list, range, or other iterable object).

This simple form of list comprehension generates a new list by evaluating the expression for each element in the iterable.

Example: Creating a list of squares

squares = [x**2 for x in range(5)]
print(squares) 
[0, 1, 4, 9, 16]

In this example, for each value x in range(5), Python evaluates x**2 and adds the result to the new list. The result is a list of the squares of the numbers from 0 to 4.

10.3.2 List Comprehension with Conditional Logic

List comprehension can include conditional logic, allowing you to filter elements or apply an operation only when a condition is met. The syntax for adding a condition is as follows:

[expression for item in iterable if condition]

The condition is a logical statement that is evaluated for each item in the iterable. Only items for which the condition evaluates to True are included in the resulting list.

Example: Filtering even numbers

even_numbers = [x for x in range(10) if x % 2 == 0]
print(even_numbers)  
[0, 2, 4, 6, 8]

Here, only numbers that satisfy the condition x % 2 == 0 (i.e., the even numbers) are included in the resulting list.

You can also use an if-else expression within list comprehension for more complex logic:

[expression_if_true if condition else expression_if_false for item in iterable]

Example: Labeling even and odd numbers

labels = ["even" if x % 2 == 0 else "odd" for x in range(5)]
print(labels)  
['even', 'odd', 'even', 'odd', 'even']

In this case, the comprehension adds the string "even" to the list if x is divisible by 2 and "odd" otherwise.

10.3.3 Nested List Comprehension

List comprehension can also be nested to create lists from multidimensional structures, such as matrices. Nested list comprehensions are powerful but can become harder to read if not used carefully.

The basic syntax for nested list comprehension is:

[expression for item1 in iterable1 for item2 in iterable2]

Example: Flattening a matrix (a list of lists)

matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat_list = [num for row in matrix for num in row]
print(flat_list) 
[1, 2, 3, 4, 5, 6, 7, 8, 9]

In this example, we loop over each row in the matrix, and for each row, we loop over each number, adding it to a single list (flat_list).

Example: Multiplying elements in a matrix by 2

matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
doubled_matrix = [[num * 2 for num in row] for row in matrix]
print(doubled_matrix)
[[2, 4, 6], [8, 10, 12], [14, 16, 18]]

In this case, the list comprehension multiplies each element in the matrix by 2, resulting in a new matrix where all values are doubled.

10.3.4 List Comprehension with Built-in Functions

You can combine list comprehensions with built-in Python functions to perform more complex transformations and calculations. This is a common pattern when dealing with operations such as string manipulation, mathematical calculations, or other functional transformations.

Example: Using the len() function

words = ["apple", "banana", "cherry"]
word_lengths = [len(word) for word in words]
print(word_lengths)  
[5, 6, 6]

Here, the list comprehension applies the len() function to each word in the list words, resulting in a list of the word lengths.

Example: Using sum() with list comprehension Suppose we have a list of lists representing test scores for different students. We can calculate the sum of each student’s test scores using list comprehension:

scores = [[75, 80, 85], [60, 70, 75], [90, 95, 100]]
total_scores = [sum(student_scores) for student_scores in scores]
print(total_scores) 
[240, 205, 285]

In this example, sum() calculates the total score for each student, and list comprehension gathers these sums into a new list total_scores.

10.3.5 List Comprehension vs. Loops

List comprehension is often favored over traditional loops because it provides a more compact and readable syntax. However, there are cases where a loop might be more appropriate, especially when the logic is complex, or when you need to modify elements in place.

Example: Traditional loop

squares = []
for x in range(5):
    squares.append(x**2)
print(squares) 
[0, 1, 4, 9, 16]

Equivalent list comprehension:

squares = [x**2 for x in range(5)]
print(squares)  
[0, 1, 4, 9, 16]

List comprehension is faster for simple operations because it avoids the overhead of repeatedly calling append() and performing function calls. However, for more complex logic, such as multiple conditional statements or nested loops, the clarity of traditional loops might outweigh the brevity of list comprehension.

10.3.6 List Comprehension with External Libraries

List comprehension can be used effectively with other basic libraries like math or random.

Example: Using math.sqrt() with list comprehension

import math
numbers = [1, 4, 9, 16, 25]
square_roots = [math.sqrt(num) for num in numbers]
print(square_roots)  
[1.0, 2.0, 3.0, 4.0, 5.0]

In this example, math.sqrt() is applied to each number in the list numbers, resulting in a list of square roots.

Example: Using random.randint() You can also use list comprehension with the random module to generate random numbers:

import random
random_numbers = [random.randint(1, 100) for _ in range(5)]
print(random_numbers)  
[13, 63, 93, 71, 13]

Here, the _ is a placeholder variable (indicating that the value is not important), and random.randint() generates a random integer for each iteration.

10.3.7 Limitations of List Comprehension

While list comprehension is a powerful tool, there are some cases where it may not be the best choice:

  • Readability: Overusing or nesting list comprehensions can make code difficult to read and maintain, especially when working with complex logic. In such cases, using traditional loops or helper functions might result in clearer and more maintainable code.

  • Complex operations: When performing complex operations with multiple steps, it’s often better to use traditional loops to avoid confusion and improve code clarity.

  • Memory efficiency: Since list comprehensions create a new list in memory, they might not be suitable for extremely large datasets. In such cases, consider using generator expressions (which we will cover in later sections) to optimize memory usage.

10.3.8 Example: Practical Applications of List Comprehension

To conclude, let’s explore a practical application of list comprehension in data processing.

Example: Filtering and transforming data Suppose we are processing a list of student scores, and we want to filter out scores below 50 and increase the remaining scores by 10%. We can accomplish this efficiently with list comprehension.

scores = [45, 67, 85, 30, 78, 92, 40]
adjusted_scores = [score * 1.1 for score in scores if score >= 50]
print(adjusted_scores)  
[73.7, 93.50000000000001, 85.80000000000001, 101.2]

In this example, only scores greater than or equal to 50 are included, and each selected score is increased by 10%. List comprehension simplifies the process of filtering and transforming the data in a single readable line.

10.4 Exercises

Excersice 1: Basic List Operations

  1. Create a list named fruits with the values: “apple”, “banana”, “cherry”.
  2. Add the fruit “orange” to the list.
  3. Remove “banana” from the list.
  4. Print the second fruit in the list.
  5. Print the length of the list.

Excersice 2: Modifying Lists

  1. Create a list numbers containing the values 1, 2, 3, 4, and 5.
  2. Replace the third element in the list with the value 10.
  3. Add the number 6 to the end of the list.
  4. Insert the number 0 at the beginning of the list.
  5. Print the updated list.

Excersice 3: Slicing Lists

Given the list colors = ["red", "green", "blue", "yellow", "purple"],
a. Slice and print the first three colors.
b. Slice and print the last two colors.
c. Slice and print every second color in the list.

Excersice 4: Nested Lists

Create a nested list matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]].
a. Print the element at the first row and second column.
b. Change the element at the second row and third column to 10.
c. Print the entire second row.

Excersice 5: Basic Tuple Operations

  1. Create a tuple my_tuple with values: 10, 20, 30, 40, 50.
  2. Print the value at index 2.
  3. Try to change the value at index 1 to 15 (what happens?).
  4. Print the length of the tuple.

Excersice 6: Tuple Unpacking

  1. Create a tuple dimensions = (1920, 1080).
  2. Unpack the tuple into variables width and height.
  3. Print the values of width and height.

Excersice 7: Returning Tuples from Functions

  1. Write a function calculate_stats(numbers) that takes a list of numbers and returns a tuple containing the sum and the average of the list.
  2. Call the function with the list [10, 20, 30, 40, 50] and unpack the returned tuple into variables total_sum and average.
  3. Print the values of total_sum and average.

Excersice 8: Nested Tuples

Given the nested tuple nested = ((1, 2), (3, 4), (5, 6)),
a. Print the first element of the second tuple.
b. Try to change the second element of the third tuple to 7 (what happens?).

Excersice 9: Basic List Comprehension

  1. Create a list comprehension that generates a list of squares of numbers from 1 to 10.
  2. Create a list comprehension that generates a list of even numbers between 1 and 20.

Excersice 10: Basic List Comprehension

  1. Create a list comprehension that generates a list of all numbers between 1 and 50 that are divisible by 3.
  2. Create a list comprehension that generates a list of all numbers between 1 and 100 that are divisible by both 2 and 5.

Excersice 11: Nested List Comprehension

Using nested list comprehension, create a list of all possible combinations of two numbers, where the first number is from the list [1, 2, 3] and the second number is from the list [4, 5, 6].

Excersice 12: String Manipulation with List Comprehension

Given the list words = ["apple", "banana", "cherry", "date"],
a. Create a list comprehension that returns the lengths of each word in the list.
b. Create a list comprehension that converts each word in the list to uppercase.

Excersice 13: Tuples and List Comprehension

Given a list of tuples representing students and their scores:
students = [("Alice", 85), ("Bob", 60), ("Charlie", 95), ("David", 70)],
a. Use a list comprehension to create a list of names of students who scored 70 or above.
b. Use a list comprehension to create a list of tuples where each student’s score is increased by 5 points.

Excersice 14: Matrix Flattening

Given a nested list (matrix): matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]], use list comprehension to flatten the matrix into a single list.