14. Lists: Ordered Collections of Items
So far in this book, we've worked with individual pieces of data: single numbers, strings, and boolean values. But real programs often need to work with collections of related items—a list of student names, a series of temperature readings, a collection of product prices, or a sequence of user commands. Python's list is the fundamental tool for storing and working with ordered collections of data.
A list is a sequence that can hold multiple items in a specific order. Unlike strings (which can only contain characters), lists can contain any type of data: numbers, strings, booleans, or even other lists. Lists are also mutable, meaning you can change their contents after creation—adding items, removing items, or modifying existing ones.
In this chapter, we'll explore how to create lists, access their elements, modify them, and use them to solve practical programming problems. By the end, you'll understand why lists are one of Python's most powerful and frequently used data structures.
14.1) Creating Lists and Accessing Elements
14.1.1) Creating Lists with Square Brackets
The most common way to create a list is by enclosing items in square brackets [], with items separated by commas. Here's a simple example:
# A list of student names
students = ["Alice", "Bob", "Charlie", "Diana"]
print(students) # Output: ['Alice', 'Bob', 'Charlie', 'Diana']Notice how Python displays the list: it shows the square brackets and puts quotes around each string. This is the representation of the list—how Python shows you what's inside.
Lists can contain any type of data. Here's a list of test scores:
# A list of integer scores
scores = [85, 92, 78, 95, 88]
print(scores) # Output: [85, 92, 78, 95, 88]You can even mix different types in the same list, though this is less common in practice:
# A mixed-type list (less common but valid)
mixed_data = ["Alice", 25, True, 3.14]
print(mixed_data) # Output: ['Alice', 25, True, 3.14]An empty list contains no items and is created with just the square brackets:
# An empty list
empty = []
print(empty) # Output: []
print(len(empty)) # Output: 0The len() function, which we've used with strings, also works with lists—it returns the number of items in the list.
14.1.2) Understanding List Order and Positions
Lists maintain the order in which you add items. The first item you put in stays first, the second stays second, and so on. This ordering is crucial because it lets you access specific items by their position (also called their index).
Python uses zero-based indexing: the first item is at position 0, the second at position 1, and so on. This might seem unusual at first, but it's a convention used by many programming languages.
Let's see how this works in practice:
students = ["Alice", "Bob", "Charlie", "Diana"]
# Access the first student (index 0)
first_student = students[0]
print(first_student) # Output: Alice
# Access the third student (index 2)
third_student = students[2]
print(third_student) # Output: CharlieNotice that to get the third student, we use index 2, not 3. This is because counting starts at 0.
14.1.3) Accessing Elements with Positive Indices
To access a list element, write the list name followed by the index in square brackets: list_name[index]. The index must be an integer within the valid range (0 to len(list) - 1).
Here's a practical example working with product prices:
# Product prices in dollars
prices = [19.99, 24.50, 15.75, 32.00, 8.99]
# Access specific prices
first_price = prices[0]
last_index = len(prices) - 1 # Calculate the last valid index
last_price = prices[last_index]
print(f"First product costs: ${first_price}") # Output: First product costs: $19.99
print(f"Last product costs: ${last_price}") # Output: Last product costs: $8.99Why do we use len(prices) - 1 for the last index? Because if a list has 5 items, the indices are 0, 1, 2, 3, 4—the last valid index is always one less than the length.
You can also use indices in expressions and calculations:
scores = [85, 92, 78, 95, 88]
# Calculate the average of the first three scores
first_three_average = (scores[0] + scores[1] + scores[2]) / 3
print(f"Average of first three: {first_three_average}") # Output: Average of first three: 85.014.1.4) Negative Indices: Counting from the End
Python provides a convenient feature: negative indices let you access items from the end of the list. Index -1 refers to the last item, -2 to the second-to-last, and so on.
students = ["Alice", "Bob", "Charlie", "Diana"]
# Access from the end
last_student = students[-1]
second_to_last = students[-2]
print(last_student) # Output: Diana
print(second_to_last) # Output: CharlieThis is particularly useful when you want the last item but don't want to calculate len(list) - 1:
prices = [19.99, 24.50, 15.75, 32.00, 8.99]
# These two approaches are equivalent
last_price_method1 = prices[len(prices) - 1]
last_price_method2 = prices[-1]
print(last_price_method1) # Output: 8.99
print(last_price_method2) # Output: 8.99Here's how positive and negative indices map to the same items:
14.1.5) What Happens with Invalid Indices
If you try to access an index that doesn't exist, Python raises an IndexError:
students = ["Alice", "Bob", "Charlie"]
# WARNING: This list has indices 0, 1, 2 (or -3, -2, -1) - for demonstration only
# Trying to access index 3 causes an error
# PROBLEM: Index 3 doesn't exist in a 3-item list
# print(students[3]) # IndexError: list index out of rangeThis error is Python's way of telling you that you've asked for an item that isn't there.
14.2) List Indexing and Slicing
14.2.1) Understanding List Slicing Basics
Just as we can slice strings (as we learned in Chapter 5), we can slice lists to extract portions of them. A slice creates a new list containing a subset of the original list's elements. The syntax is list[start:stop], where start is the index where the slice begins (inclusive) and stop is where it ends (exclusive).
numbers = [10, 20, 30, 40, 50, 60, 70]
# Get elements from index 1 up to (but not including) index 4
subset = numbers[1:4]
print(subset) # Output: [20, 30, 40]The slice [1:4] includes indices 1, 2, and 3, but stops before index 4. This "stop is exclusive" rule is the same as with string slicing.
Let's see a practical example with student names:
students = ["Alice", "Bob", "Charlie", "Diana", "Eve", "Frank"]
# Get the first three students
first_three = students[0:3]
print(first_three) # Output: ['Alice', 'Bob', 'Charlie']
# Get students from index 2 to 4
middle_group = students[2:5]
print(middle_group) # Output: ['Charlie', 'Diana', 'Eve']14.2.2) Omitting Start or Stop in Slices
You can omit the start index to slice from the beginning, or omit the stop index to slice to the end:
scores = [85, 92, 78, 95, 88, 91, 87]
# From the beginning up to index 3
first_few = scores[:3]
print(first_few) # Output: [85, 92, 78]
# From index 4 to the end
last_few = scores[4:]
print(last_few) # Output: [88, 91, 87]
# The entire list (from beginning to end)
all_scores = scores[:]
print(all_scores) # Output: [85, 92, 78, 95, 88, 91, 87]The slice [:] creates a copy of the entire list. This is useful when you want to work with a duplicate without modifying the original—we'll explore this more in section 14.6.
14.2.3) Using Negative Indices in Slices
Negative indices work in slices just as they do with single-element access. This is particularly useful for getting items from the end:
students = ["Alice", "Bob", "Charlie", "Diana", "Eve", "Frank"]
# Get the last three students
last_three = students[-3:]
print(last_three) # Output: ['Diana', 'Eve', 'Frank']
# Get all but the last two students
all_but_last_two = students[:-2]
print(all_but_last_two) # Output: ['Alice', 'Bob', 'Charlie', 'Diana']
# Get from the third-to-last to the second-to-last
middle_from_end = students[-3:-1]
print(middle_from_end) # Output: ['Diana', 'Eve']14.2.4) Slicing with a Step Value
You can add a third parameter to control the step (how many indices to skip between items). The full syntax is list[start:stop:step]:
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# Every second number starting from index 0
evens = numbers[0:10:2]
print(evens) # Output: [0, 2, 4, 6, 8]
# Every third number starting from index 1
every_third = numbers[1:10:3]
print(every_third) # Output: [1, 4, 7]You can also use a negative step to reverse the list:
numbers = [1, 2, 3, 4, 5]
# Reverse the list
reversed_numbers = numbers[::-1]
print(reversed_numbers) # Output: [5, 4, 3, 2, 1]The slice [::-1] means "start at the end, go to the beginning, stepping backward by 1." This is a common Python idiom for reversing sequences.
14.2.5) Slices Never Cause IndexError
Unlike accessing a single element, slicing is very forgiving. If you specify indices outside the list's range, Python simply adjusts them to fit:
numbers = [10, 20, 30, 40, 50]
# Asking for more than exists
extended_slice = numbers[2:100]
print(extended_slice) # Output: [30, 40, 50]
# Starting beyond the end
empty_slice = numbers[10:20]
print(empty_slice) # Output: []This behavior is useful because it means you don't have to worry about exact boundaries when slicing—Python handles edge cases gracefully.
14.3) Modifying Lists and Common List Methods
14.3.1) Lists Are Mutable: Changing Elements
Unlike strings, which are immutable, lists are mutable—you can change their contents after creation. You can modify individual elements by assigning new values to specific indices:
# Start with a list of prices
prices = [19.99, 24.50, 15.75, 32.00]
print(prices) # Output: [19.99, 24.5, 15.75, 32.0]
# Update the second price (index 1)
prices[1] = 22.99
print(prices) # Output: [19.99, 22.99, 15.75, 32.0]
# Update the last price using negative indexing
prices[-1] = 29.99
print(prices) # Output: [19.99, 22.99, 15.75, 29.99]This mutability is powerful—it means you can update data in place without creating new lists. However, it also means you need to be careful about unintended changes, which we'll discuss in section 14.6.
14.3.2) Adding Elements with append()
The append() method adds a single item to the end of a list. This is one of the most frequently used list operations:
# Start with an empty shopping cart
cart = []
print(cart) # Output: []
# Add items one by one
cart.append("Milk")
print(cart) # Output: ['Milk']
cart.append("Bread")
print(cart) # Output: ['Milk', 'Bread']
cart.append("Eggs")
print(cart) # Output: ['Milk', 'Bread', 'Eggs']Notice that append() modifies the list in place—it doesn't return a new list. The method returns None, so you don't need to assign its result:
scores = [85, 92, 78]
result = scores.append(95)
print(scores) # Output: [85, 92, 78, 95]
print(result) # Output: None14.3.3) Inserting Elements at Specific Positions with insert()
While append() always adds to the end, insert() lets you add an item at any position. The syntax is list.insert(index, item):
students = ["Alice", "Charlie", "Diana"]
print(students) # Output: ['Alice', 'Charlie', 'Diana']
# Insert "Bob" at index 1 (between Alice and Charlie)
students.insert(1, "Bob")
print(students) # Output: ['Alice', 'Bob', 'Charlie', 'Diana']When you insert at an index, the item currently at that position (and all items after it) shift to the right:
numbers = [10, 20, 30, 40]
print(numbers) # Output: [10, 20, 30, 40]
# Insert 25 at index 2
numbers.insert(2, 25)
print(numbers) # Output: [10, 20, 25, 30, 40]You can insert at the beginning by using index 0:
priorities = ["Medium", "Low"]
priorities.insert(0, "High")
print(priorities) # Output: ['High', 'Medium', 'Low']If you specify an index beyond the list's length, insert() simply adds the item at the end (like append()):
items = [1, 2, 3]
items.insert(100, 4)
print(items) # Output: [1, 2, 3, 4]14.3.4) Removing Elements with remove()
The remove() method removes the first occurrence of a specific value from the list:
fruits = ["apple", "banana", "cherry", "banana", "date"]
print(fruits) # Output: ['apple', 'banana', 'cherry', 'banana', 'date']
# Remove the first "banana"
fruits.remove("banana")
print(fruits) # Output: ['apple', 'cherry', 'banana', 'date']Notice that only the first "banana" was removed—the second one remains. If you try to remove a value that doesn't exist, Python raises a ValueError:
numbers = [10, 20, 30]
# WARNING: Attempting to remove non-existent value - for demonstration only
# PROBLEM: 40 is not in the list
# numbers.remove(40) # ValueError: list.remove(x): x not in listTo avoid this error, you can check if the item exists before removing it:
cart = ["Milk", "Bread", "Eggs"]
item_to_remove = "Butter"
if item_to_remove in cart:
cart.remove(item_to_remove)
print(f"Removed {item_to_remove}")
else:
print(f"{item_to_remove} not in cart")
# Output: Butter not in cart14.3.5) Removing and Returning Elements with pop()
The pop() method removes an item at a specific index and returns it. If you don't specify an index, it removes and returns the last item:
scores = [85, 92, 78, 95, 88]
# Remove and get the last score
last_score = scores.pop()
print(f"Removed: {last_score}") # Output: Removed: 88
print(scores) # Output: [85, 92, 78, 95]
# Remove and get the score at index 1
second_score = scores.pop(1)
print(f"Removed: {second_score}") # Output: Removed: 92
print(scores) # Output: [85, 78, 95]This is useful when you need to process items from a list one at a time:
tasks = ["Write code", "Test code", "Deploy code"]
while len(tasks) > 0:
current_task = tasks.pop(0) # Remove from the beginning
print(f"Working on: {current_task}")
# Output:
# Working on: Write code
# Working on: Test code
# Working on: Deploy code
print(tasks) # Output: []14.3.6) Extending Lists with extend()
The extend() method adds all items from another list (or any iterable) to the end of the current list:
primary_colors = ["red", "blue", "yellow"]
secondary_colors = ["green", "orange", "purple"]
# Add all secondary colors to primary colors
primary_colors.extend(secondary_colors)
print(primary_colors)
# Output: ['red', 'blue', 'yellow', 'green', 'orange', 'purple']This is different from append(), which would add the entire list as a single element:
colors1 = ["red", "blue"]
colors2 = ["green", "orange"]
# Using append (adds the list as one element)
colors1.append(colors2)
print(colors1) # Output: ['red', 'blue', ['green', 'orange']]
# Using extend (adds each element individually)
colors3 = ["red", "blue"]
colors3.extend(colors2)
print(colors3) # Output: ['red', 'blue', 'green', 'orange']14.3.7) Sorting Lists with sort() and sorted()
Python provides two ways to sort lists. The sort() method sorts the list in place (modifying the original):
scores = [78, 95, 85, 92, 88]
scores.sort()
print(scores) # Output: [78, 85, 88, 92, 95]To sort in descending order, use the reverse parameter:
scores = [78, 95, 85, 92, 88]
scores.sort(reverse=True)
print(scores) # Output: [95, 92, 88, 85, 78]The sorted() function (which we'll explore more in Chapter 38) creates a new sorted list without modifying the original:
original = [78, 95, 85, 92, 88]
sorted_scores = sorted(original)
print(original) # Output: [78, 95, 85, 92, 88]
print(sorted_scores) # Output: [78, 85, 88, 92, 95]Sorting works with strings too, using alphabetical order:
names = ["Charlie", "Alice", "Diana", "Bob"]
names.sort()
print(names) # Output: ['Alice', 'Bob', 'Charlie', 'Diana']14.3.8) Reversing Lists with reverse()
The reverse() method reverses the list in place:
numbers = [1, 2, 3, 4, 5]
numbers.reverse()
print(numbers) # Output: [5, 4, 3, 2, 1]This is different from sorting in reverse order—reverse() simply flips the current order, whatever it may be:
mixed = [3, 1, 4, 1, 5]
mixed.reverse()
print(mixed) # Output: [5, 1, 4, 1, 3]Remember that you can also reverse a list using slicing: list[::-1]. The difference is that slicing creates a new list, while reverse() modifies the original.
14.3.9) Finding Elements with index() and count()
The index() method returns the position of the first occurrence of a value:
students = ["Alice", "Bob", "Charlie", "Diana", "Bob"]
# Find where "Charlie" is
position = students.index("Charlie")
print(f"Charlie is at index {position}") # Output: Charlie is at index 2
# Find the first "Bob"
bob_position = students.index("Bob")
print(f"Bob is at index {bob_position}") # Output: Bob is at index 1If the value doesn't exist, index() raises a ValueError:
students = ["Alice", "Bob", "Charlie"]
# WARNING: Attempting to find non-existent value - for demonstration only
# PROBLEM: 'Eve' is not in the list
# position = students.index("Eve") # ValueError: 'Eve' is not in listThe count() method returns how many times a value appears:
numbers = [1, 2, 3, 2, 4, 2, 5]
twos = numbers.count(2)
print(f"The number 2 appears {twos} times") # Output: The number 2 appears 3 times
# Count can return 0 if the item doesn't exist
sixes = numbers.count(6)
print(f"The number 6 appears {sixes} times") # Output: The number 6 appears 0 times14.3.10) Clearing All Elements with clear()
The clear() method removes all items from a list, leaving it empty:
cart = ["Milk", "Bread", "Eggs", "Butter"]
print(cart) # Output: ['Milk', 'Bread', 'Eggs', 'Butter']
cart.clear()
print(cart) # Output: []
print(len(cart)) # Output: 0This is equivalent to assigning an empty list, but clear() is more explicit about the intention.
14.4) Deleting List Elements with del
14.4.1) Using del to Remove Elements by Index
The del statement can delete list elements at specific indices:
students = ["Alice", "Bob", "Charlie", "Diana", "Eve"]
print(students) # Output: ['Alice', 'Bob', 'Charlie', 'Diana', 'Eve']
# Delete the element at index 2
del students[2]
print(students) # Output: ['Alice', 'Bob', 'Diana', 'Eve']Unlike pop(), del doesn't return the removed value—it simply deletes it. This is useful when you want to remove an item but don't need to use it:
scores = [85, 92, 78, 95, 88]
# Remove the lowest score (at index 2)
del scores[2]
print(scores) # Output: [85, 92, 95, 88]You can also use negative indices with del:
tasks = ["Task 1", "Task 2", "Task 3", "Task 4"]
# Delete the last task
del tasks[-1]
print(tasks) # Output: ['Task 1', 'Task 2', 'Task 3']14.4.2) Deleting Slices with del
The del statement can remove entire slices at once:
numbers = [10, 20, 30, 40, 50, 60, 70]
print(numbers) # Output: [10, 20, 30, 40, 50, 60, 70]
# Delete elements from index 2 to 4 (indices 2, 3, 4)
del numbers[2:5]
print(numbers) # Output: [10, 20, 60, 70]This is particularly useful for removing ranges of elements:
data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# Remove the first three elements
del data[:3]
print(data) # Output: [4, 5, 6, 7, 8, 9, 10]
# Remove the last two elements
del data[-2:]
print(data) # Output: [4, 5, 6, 7, 8]You can even delete every other element using step slicing:
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# Delete every second element
del numbers[::2]
print(numbers) # Output: [1, 3, 5, 7, 9]14.4.3) Comparing del, remove(), and pop()
Let's clarify when to use each deletion method:
# Example list for comparison
items = ["apple", "banana", "cherry", "date", "elderberry"]
# Use remove() when you know the VALUE to delete
items_copy1 = items.copy()
items_copy1.remove("cherry") # Removes first "cherry"
print(items_copy1) # Output: ['apple', 'banana', 'date', 'elderberry']
# Use pop() when you know the INDEX and need the value
items_copy2 = items.copy()
removed_item = items_copy2.pop(2) # Removes and returns item at index 2
print(f"Removed: {removed_item}") # Output: Removed: cherry
print(items_copy2) # Output: ['apple', 'banana', 'date', 'elderberry']
# Use del when you know the INDEX but don't need the value
items_copy3 = items.copy()
del items_copy3[2] # Just removes item at index 2
print(items_copy3) # Output: ['apple', 'banana', 'date', 'elderberry']14.5) Iterating Over Lists with for Loops
14.5.1) Basic List Iteration
One of the most common operations with lists is processing each item in sequence. The for loop (which we learned in Chapter 12) is perfect for this:
students = ["Alice", "Bob", "Charlie", "Diana"]
# Process each student
for student in students:
print(f"Hello, {student}!")
# Output:
# Hello, Alice!
# Hello, Bob!
# Hello, Charlie!
# Hello, Diana!The loop variable (student in this case) takes on each value from the list, one at a time, in order. You can name this variable anything meaningful:
scores = [85, 92, 78, 95, 88]
# Calculate and display each score's grade
for score in scores:
if score >= 90:
grade = "A"
elif score >= 80:
grade = "B"
else:
grade = "C"
print(f"Score {score} is a {grade}")
# Output:
# Score 85 is a B
# Score 92 is a A
# Score 78 is a C
# Score 95 is a A
# Score 88 is a B14.5.2) Processing Corresponding Items from Multiple Lists
Sometimes you need to work with related data stored in separate lists. We'll learn about the zip() function in detail in Chapter 38, but here's a brief preview of how it can help process corresponding items:
# We'll learn about zip() in Chapter 38, but for now, here's a simple example
students = ["Alice", "Bob", "Charlie"]
scores = [85, 92, 78]
# Process corresponding pairs
for student, score in zip(students, scores):
print(f"{student} scored {score}")
# Output:
# Alice scored 85
# Bob scored 92
# Charlie scored 78The zip() function pairs up elements from multiple lists, which is useful when you have related data in separate lists. We'll explore this and other iteration tools in depth in Chapter 38.
14.6) Copying Lists and Avoiding Shared References
14.6.1) Understanding List References
When you assign a list to a variable, Python doesn't create a copy of the list—it creates a reference to the same list object in memory. This means multiple variables can refer to the same list:
original = [1, 2, 3]
reference = original # Both variables point to the SAME list
# Modifying through one variable affects the other
reference.append(4)
print(original) # Output: [1, 2, 3, 4]
print(reference) # Output: [1, 2, 3, 4]This behavior can be surprising if you expect reference to be an independent copy. Let's see why this matters:
# Scenario: You want to track changes to a shopping cart
cart = ["Milk", "Bread"]
backup = cart # Trying to save the original state
# Add more items
cart.append("Eggs")
cart.append("Butter")
# Check the "backup"
print(backup) # Output: ['Milk', 'Bread', 'Eggs', 'Butter']The backup changed too! This is because backup and cart are two names for the same list object.
14.6.2) Creating Independent Copies with Slicing
To create a true independent copy, use slicing with [:]:
original = [1, 2, 3]
copy = original[:] # Creates a NEW list with the same contents
# Modifying the copy doesn't affect the original
copy.append(4)
print(original) # Output: [1, 2, 3]
print(copy) # Output: [1, 2, 3, 4]Now let's fix our shopping cart example:
cart = ["Milk", "Bread"]
backup = cart[:] # Create an independent copy
# Add more items to cart
cart.append("Eggs")
cart.append("Butter")
# The backup remains unchanged
print(cart) # Output: ['Milk', 'Bread', 'Eggs', 'Butter']
print(backup) # Output: ['Milk', 'Bread']14.6.3) Creating Copies with the copy() Method
Lists also have a copy() method that does the same thing as [:]:
original = [10, 20, 30]
copy = original.copy()
copy.append(40)
print(original) # Output: [10, 20, 30]
print(copy) # Output: [10, 20, 30, 40]Both [:] and copy() create shallow copies, which we'll discuss next.
14.6.4) The Shallow Copy Limitation
Both [:] and copy() create shallow copies. This means they copy the list structure, but if the list contains other mutable objects (like other lists), those inner objects are still shared:
# A list containing lists
original = [[1, 2], [3, 4], [5, 6]]
copy = original[:]
# Modifying the outer list structure is independent
copy.append([7, 8])
print(original) # Output: [[1, 2], [3, 4], [5, 6]]
print(copy) # Output: [[1, 2], [3, 4], [5, 6], [7, 8]]
# But modifying an inner list affects both!
copy[0].append(99)
print(original) # Output: [[1, 2, 99], [3, 4], [5, 6]]
print(copy) # Output: [[1, 2, 99], [3, 4], [5, 6], [7, 8]]Why does this happen? Because the shallow copy creates a new outer list, but the inner lists are still shared references:
For nested structures, you would need a deep copy, which we'll learn about when we explore the copy module in later chapters. For now, be aware that shallow copies work perfectly for lists of immutable items (numbers, strings, tuples), but require caution with nested mutable structures.
14.6.5) When Shared References Are Useful
Sometimes you want multiple variables to refer to the same list. This is useful when you need to modify a list from different parts of your code:
# A function that modifies a list in place
def add_bonus_points(scores, bonus):
for i in range(len(scores)):
scores[i] = scores[i] + bonus
# The original list is modified
student_scores = [85, 92, 78]
add_bonus_points(student_scores, 5)
print(student_scores) # Output: [90, 97, 83]This works because the function receives a reference to the original list, not a copy. We'll explore this more when we study functions in detail in Part V.
14.7) Using enumerate() While Looping Over Lists
14.7.1) The Need for Both Index and Value
Sometimes when iterating over a list, you need both the index and the value. One approach is to use range(len(list)):
students = ["Alice", "Bob", "Charlie", "Diana"]
for i in range(len(students)):
print(f"Student {i}: {students[i]}")
# Output:
# Student 0: Alice
# Student 1: Bob
# Student 2: Charlie
# Student 3: DianaThis works, but it's not very elegant. You have to use students[i] to access each value, which is less readable than directly iterating over the values.
14.7.2) Using enumerate() for Cleaner Code
The enumerate() function provides a better solution. It returns both the index and the value for each item:
students = ["Alice", "Bob", "Charlie", "Diana"]
for index, student in enumerate(students):
print(f"Student {index}: {student}")
# Output:
# Student 0: Alice
# Student 1: Bob
# Student 2: Charlie
# Student 3: DianaThe syntax for index, value in enumerate(list) unpacks each pair that enumerate() produces. This is much more readable than using range(len()).
14.7.3) Starting enumerate() at a Different Number
By default, enumerate() starts counting at 0. You can specify a different starting number with the start parameter:
students = ["Alice", "Bob", "Charlie", "Diana"]
# Start counting at 1 instead of 0
for position, student in enumerate(students, start=1):
print(f"Position {position}: {student}")
# Output:
# Position 1: Alice
# Position 2: Bob
# Position 3: Charlie
# Position 4: DianaThis is useful when you want to display human-friendly numbering (starting at 1) rather than programmer-friendly indexing (starting at 0).
Practical Examples with enumerate()
Here's a practical example displaying a numbered menu:
menu_items = ["New Game", "Load Game", "Settings", "Quit"]
print("Main Menu:")
for number, item in enumerate(menu_items, start=1):
print(f"{number}. {item}")
# Output:
# Main Menu:
# 1. New Game
# 2. Load Game
# 3. Settings
# 4. Quit14.7.4) Modifying Lists with enumerate()
You can use enumerate() when you need to modify list elements based on their position:
# Add position-based bonus to scores
scores = [85, 92, 78, 95, 88]
for index, score in enumerate(scores):
# First student gets 5 bonus points, second gets 4, etc.
bonus = 5 - index
if bonus > 0:
scores[index] = score + bonus
print(scores) # Output: [90, 96, 81, 97, 89]14.8) Common List Patterns: Searching, Filtering, and Aggregating Data
14.8.1) Searching for Items in Lists
One of the most common tasks is checking whether a list contains a specific item. The in operator (which we learned in Chapter 7) makes this simple:
students = ["Alice", "Bob", "Charlie", "Diana"]
# Check if a student is in the list
if "Charlie" in students:
print("Charlie is enrolled") # Output: Charlie is enrolled
if "Eve" not in students:
print("Eve is not enrolled") # Output: Eve is not enrolledTo find the position of an item, use the index() method (covered in section 14.3.9), but remember to check if the item exists first:
scores = [85, 92, 78, 95, 88]
target_score = 95
if target_score in scores:
position = scores.index(target_score)
print(f"Score {target_score} found at index {position}")
# Output: Score 95 found at index 3
else:
print(f"Score {target_score} not found")14.8.2) Finding the Maximum and Minimum Values
Python's built-in max() and min() functions work with lists:
scores = [85, 92, 78, 95, 88, 91, 76]
highest_score = max(scores)
lowest_score = min(scores)
print(f"Highest score: {highest_score}") # Output: Highest score: 95
print(f"Lowest score: {lowest_score}") # Output: Lowest score: 7614.8.3) Calculating Aggregates: Sum, Average, and Count
Computing totals and averages is a fundamental list operation:
scores = [85, 92, 78, 95, 88, 91, 76, 89]
# Calculate total and average
total = sum(scores)
count = len(scores)
average = total / count
print(f"Total: {total}") # Output: Total: 694
print(f"Count: {count}") # Output: Count: 8
print(f"Average: {average:.2f}") # Output: Average: 86.75Here's a practical example calculating a shopping cart total:
cart_items = ["Milk", "Bread", "Eggs", "Butter", "Cheese"]
prices = [3.99, 2.49, 4.99, 5.49, 6.99]
# Calculate total cost
total_cost = sum(prices)
item_count = len(cart_items)
print(f"Items in cart: {item_count}")
print(f"Total cost: ${total_cost:.2f}")
# Output:
# Items in cart: 5
# Total cost: $23.9514.9) List Mutability and Truthiness in Conditions
14.9.1) Understanding List Mutability in Practice
We've seen throughout this chapter that lists are mutable—they can be changed after creation. This mutability is what makes lists so powerful for storing and manipulating collections of data. Let's consolidate our understanding with a comprehensive example:
# Start with an empty task list
tasks = []
print(f"Initial tasks: {tasks}") # Output: Initial tasks: []
# Add tasks
tasks.append("Write code")
tasks.append("Test code")
tasks.append("Deploy code")
print(f"After adding: {tasks}")
# Output: After adding: ['Write code', 'Test code', 'Deploy code']
# Insert an urgent task at the beginning
tasks.insert(0, "Review requirements")
print(f"After inserting: {tasks}")
# Output: After inserting: ['Review requirements', 'Write code', 'Test code', 'Deploy code']
# Complete and remove the first task
completed = tasks.pop(0)
print(f"Completed: {completed}") # Output: Completed: Review requirements
print(f"Remaining: {tasks}")
# Output: Remaining: ['Write code', 'Test code', 'Deploy code']
# Modify a task
tasks[1] = "Test code thoroughly"
print(f"After modifying: {tasks}")
# Output: After modifying: ['Write code', 'Test code thoroughly', 'Deploy code']14.9.2) Mutability vs Immutability: Lists vs Strings
It's important to understand the difference between mutable lists and immutable strings. With strings, operations create new strings rather than modifying the original:
# Strings are immutable
text = "hello"
text.upper() # Creates a new string, doesn't change original
print(text) # Output: hello (unchanged)
# To "change" a string, you must reassign
text = text.upper()
print(text) # Output: HELLO
# Lists are mutable
numbers = [1, 2, 3]
numbers.append(4) # Modifies the list in place
print(numbers) # Output: [1, 2, 3, 4] (changed)This difference affects how you work with these types:
# String operations require reassignment
name = "alice"
name = name.capitalize() # Must reassign to see the change
print(name) # Output: Alice
# List operations modify in place
scores = [85, 92, 78]
scores.append(95) # No reassignment needed
print(scores) # Output: [85, 92, 78, 95]14.9.3) Using Lists in Boolean Contexts
Lists have truthiness: an empty list is considered False, and any non-empty list is considered True. This is useful in conditional statements:
# Empty list is falsy
empty_cart = []
if empty_cart:
print("Cart has items")
else:
print("Cart is empty") # Output: Cart is empty
# Non-empty list is truthy
cart_with_items = ["Milk", "Bread"]
if cart_with_items:
print("Cart has items") # Output: Cart has itemsThis pattern is commonly used to check if a list has any elements before processing:
students = ["Alice", "Bob", "Charlie"]
if students:
print(f"We have {len(students)} students")
for student in students:
print(f" - {student}")
else:
print("No students enrolled")
# Output:
# We have 3 students
# - Alice
# - Bob
# - Charlie14.9.4) Practical Pattern: Processing Until Empty
The truthiness of lists enables a useful pattern for processing items until a list is empty:
# Process tasks until none remain
tasks = ["Task 1", "Task 2", "Task 3"]
while tasks: # Continue while list is not empty
current_task = tasks.pop(0)
print(f"Processing: {current_task}")
print("All tasks completed!")
# Output:
# Processing: Task 1
# Processing: Task 2
# Processing: Task 3
# All tasks completed!14.9.5) Checking for Empty Lists: Explicit vs Implicit
There are two ways to check if a list is empty:
items = []
# Implicit check (Pythonic)
if not items:
print("List is empty") # Output: List is empty
# Explicit check (also valid)
if len(items) == 0:
print("List is empty") # Output: List is emptyThe implicit check (if not items:) is generally preferred in Python because it's more concise and works with any collection type. However, both approaches are correct and you'll see both in real code.
14.9.6) Mutability and Function Behavior
When you pass a list to a function (which we'll explore in detail in Part V), the function receives a reference to the same list object. This means the function can modify the original list:
def add_item(shopping_list, item):
shopping_list.append(item)
print(f"Added {item}")
# The original list is modified
cart = ["Milk", "Bread"]
print(f"Before: {cart}") # Output: Before: ['Milk', 'Bread']
add_item(cart, "Eggs") # Output: Added Eggs
print(f"After: {cart}") # Output: After: ['Milk', 'Bread', 'Eggs']This behavior is different from immutable types like strings and numbers, where the original value cannot be changed by a function. Understanding this distinction is crucial for writing correct programs.
Lists are one of Python's most fundamental and versatile data structures. They provide an ordered, mutable collection that can grow and shrink as needed, making them perfect for storing and processing sequences of related data. You've learned how to create lists, access their elements through indexing and slicing, modify them with various methods, iterate over them efficiently, and understand their mutable nature.
The patterns we've explored—searching, filtering, aggregating, and transforming data—form the foundation for working with collections in Python. As you continue learning, you'll discover even more powerful ways to work with lists, including list comprehensions (Chapter 35) and advanced iteration techniques (Chapters 36-37). But the fundamentals you've mastered in this chapter will serve you well throughout your Python programming journey.