Python & AI Tutorials Logo
Python Programming

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:

python
# 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:

python
# 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:

python
# 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:

python
# An empty list
empty = []
print(empty)  # Output: []
print(len(empty))  # Output: 0

The 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.

List: ['Alice', 'Bob', 'Charlie', 'Diana']

Index 0: 'Alice'

Index 1: 'Bob'

Index 2: 'Charlie'

Index 3: 'Diana'

Let's see how this works in practice:

python
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: Charlie

Notice 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:

python
# 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.99

Why 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:

python
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.0

14.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.

python
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: Charlie

This is particularly useful when you want the last item but don't want to calculate len(list) - 1:

python
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.99

Here's how positive and negative indices map to the same items:

List: ['Alice', 'Bob', 'Charlie', 'Diana']

Positive: 0, 1, 2, 3

Negative: -4, -3, -2, -1

Both refer to 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:

python
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 range

This 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).

python
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:

python
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:

python
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:

python
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]:

python
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:

python
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:

python
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:

python
# 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:

python
# 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:

python
scores = [85, 92, 78]
result = scores.append(95)
 
print(scores)   # Output: [85, 92, 78, 95]
print(result)   # Output: None

14.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):

python
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:

python
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:

python
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()):

python
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:

python
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:

python
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 list

To avoid this error, you can check if the item exists before removing it:

python
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 cart

14.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:

python
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:

python
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:

python
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:

python
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):

python
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:

python
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:

python
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:

python
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:

python
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:

python
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:

python
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 1

If the value doesn't exist, index() raises a ValueError:

python
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 list

The count() method returns how many times a value appears:

python
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 times

14.3.10) Clearing All Elements with clear()

The clear() method removes all items from a list, leaving it empty:

python
cart = ["Milk", "Bread", "Eggs", "Butter"]
print(cart)  # Output: ['Milk', 'Bread', 'Eggs', 'Butter']
 
cart.clear()
print(cart)  # Output: []
print(len(cart))  # Output: 0

This 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:

python
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:

python
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:

python
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:

python
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:

python
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:

python
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:

python
# 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:

python
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:

python
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 B

14.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:

python
# 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 78

The 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:

python
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:

python
# 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.

Variable: cart

List Object: ⦗'Milk', 'Bread', 'Eggs', 'Butter'⦘

Variable: backup

14.6.2) Creating Independent Copies with Slicing

To create a true independent copy, use slicing with [:]:

python
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:

python
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 [:]:

python
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:

python
# 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:

original outer list

Inner list: ⦗1, 2, 99⦘

Inner list: ⦗3, 4⦘

Inner list: ⦗5, 6⦘

copy outer list

Inner list: ⦗7, 8⦘

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:

python
# 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)):

python
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: Diana

This 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:

python
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: Diana

The 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:

python
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: Diana

This 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:

python
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. Quit

14.7.4) Modifying Lists with enumerate()

You can use enumerate() when you need to modify list elements based on their position:

python
# 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:

python
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 enrolled

To 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:

python
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:

python
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: 76

14.8.3) Calculating Aggregates: Sum, Average, and Count

Computing totals and averages is a fundamental list operation:

python
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.75

Here's a practical example calculating a shopping cart total:

python
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.95

14.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:

python
# 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:

python
# 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:

python
# 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:

python
# 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 items

This pattern is commonly used to check if a list has any elements before processing:

python
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
#   - Charlie

14.9.4) Practical Pattern: Processing Until Empty

The truthiness of lists enables a useful pattern for processing items until a list is empty:

python
# 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:

python
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 empty

The 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:

python
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.

© 2025. Primesoft Co., Ltd.
support@primesoft.ai