Lists and List Operations
Lists are one of the most versatile and commonly used data structures in Python. A list is an ordered, mutable collection of elements that can be of different types. This flexibility makes lists extremely useful for a wide range of programming tasks.
Creating Lists
There are several ways to create lists in Python:
# Empty list
empty_list = []
empty_list_alt = list() # Using the list() constructor
# List with initial values
fruits = ["apple", "banana", "cherry"]
# List with mixed data types
mixed_list = [1, "hello", 3.14, True]
# Nested lists
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
# Creating a list from another iterable
letters = list("hello") # Creates ['h', 'e', 'l', 'l', 'o']
numbers = list(range(1, 6)) # Creates [1, 2, 3, 4, 5]
Accessing List Elements
List elements are indexed starting from 0 for the first element:
fruits = ["apple", "banana", "cherry", "date", "elderberry"]
# Accessing by positive index (from the beginning)
first_fruit = fruits[0] # "apple"
second_fruit = fruits[1] # "banana"
# Accessing by negative index (from the end)
last_fruit = fruits[-1] # "elderberry"
second_last = fruits[-2] # "date"
# Accessing nested lists
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
element = matrix[1][2] # 6 (row 1, column 2)
Important:
Trying to access an index that doesn’t exist will raise an IndexError
. Always ensure your indices are within the valid range or use error handling.
fruits = ["apple", "banana", "cherry"]
try:
fourth_fruit = fruits[3] # IndexError: list index out of range
except IndexError as e:
print(f"Error: {e}")
List Slicing
Slicing allows you to extract a portion of a list:
fruits = ["apple", "banana", "cherry", "date", "elderberry"]
# Syntax: list[start:stop:step]
# start: inclusive, stop: exclusive, step: increment (default 1)
# Get a slice from index 1 to 3 (exclusive)
slice1 = fruits[1:3] # ["banana", "cherry"]
# Omitting start means start from the beginning
slice2 = fruits[:3] # ["apple", "banana", "cherry"]
# Omitting stop means go to the end
slice3 = fruits[2:] # ["cherry", "date", "elderberry"]
# Negative indices in slices
slice4 = fruits[-3:-1] # ["cherry", "date"]
# Using step to get every 2nd element
slice5 = fruits[::2] # ["apple", "cherry", "elderberry"]
# Reverse a list
reversed_fruits = fruits[::-1] # ["elderberry", "date", "cherry", "banana", "apple"]
Note: Slicing creates a new list, so the original list remains unchanged.
Modifying Lists
Lists are mutable, meaning you can change their content:
Changing Individual Elements
fruits = ["apple", "banana", "cherry"]
# Change the second element
fruits[1] = "blueberry"
print(fruits) # ["apple", "blueberry", "cherry"]
# Change a slice
fruits[0:2] = ["avocado", "blackberry"]
print(fruits) # ["avocado", "blackberry", "cherry"]
# Insert multiple elements in place of one
fruits[1:2] = ["boysenberry", "blackcurrant"]
print(fruits) # ["avocado", "boysenberry", "blackcurrant", "cherry"]
# Remove elements by assigning an empty list
fruits[1:3] = []
print(fruits) # ["avocado", "cherry"]
Adding Elements
fruits = ["apple", "banana"]
# Append adds a single element to the end
fruits.append("cherry")
print(fruits) # ["apple", "banana", "cherry"]
# Insert adds an element at a specific position
fruits.insert(1, "blueberry") # Insert at index 1
print(fruits) # ["apple", "blueberry", "banana", "cherry"]
# Extend adds multiple elements from another iterable
more_fruits = ["date", "elderberry"]
fruits.extend(more_fruits)
print(fruits) # ["apple", "blueberry", "banana", "cherry", "date", "elderberry"]
# Concatenation with + operator (creates a new list)
fruits = ["apple", "banana"]
more_fruits = ["cherry", "date"]
all_fruits = fruits + more_fruits
print(all_fruits) # ["apple", "banana", "cherry", "date"]
Important:
The append()
method adds the entire object as a single element, while extend()
adds each element of the iterable individually:
list1 = [1, 2, 3]
list2 = [4, 5]
# Using append
list1.append(list2)
print(list1) # [1, 2, 3, [4, 5]]
# Using extend
list1 = [1, 2, 3] # Reset list1
list1.extend(list2)
print(list1) # [1, 2, 3, 4, 5]
Removing Elements
fruits = ["apple", "banana", "cherry", "banana", "date"]
# Remove by value (first occurrence)
fruits.remove("banana")
print(fruits) # ["apple", "cherry", "banana", "date"]
# Remove by index and get the value
removed_fruit = fruits.pop(1) # Removes item at index 1
print(removed_fruit) # "cherry"
print(fruits) # ["apple", "banana", "date"]
# Remove the last item if no index is specified
last_fruit = fruits.pop()
print(last_fruit) # "date"
print(fruits) # ["apple", "banana"]
# Clear all elements
fruits.clear()
print(fruits) # []
# Delete list items or the entire list with del
fruits = ["apple", "banana", "cherry"]
del fruits[1]
print(fruits) # ["apple", "cherry"]
del fruits # Deletes the entire list
# print(fruits) # NameError: name 'fruits' is not defined
List Methods
Python provides many built-in methods for lists:
Finding Elements
fruits = ["apple", "banana", "cherry", "banana", "date"]
# Check if an element exists
if "banana" in fruits:
print("Yes, 'banana' is in the list")
# Count occurrences
banana_count = fruits.count("banana")
print(f"Banana appears {banana_count} times") # 2
# Find the index of an element (first occurrence)
banana_index = fruits.index("banana")
print(f"First banana is at index {banana_index}") # 1
# Find subsequent occurrences
second_banana_index = fruits.index("banana", banana_index + 1)
print(f"Second banana is at index {second_banana_index}") # 3
# To avoid errors if the element doesn't exist
try:
grape_index = fruits.index("grape")
except ValueError:
print("Grape not found in the list")
Sorting Lists
numbers = [3, 1, 4, 1, 5, 9, 2, 6]
# Sort in place
numbers.sort()
print(numbers) # [1, 1, 2, 3, 4, 5, 6, 9]
# Sort in descending order
numbers.sort(reverse=True)
print(numbers) # [9, 6, 5, 4, 3, 2, 1, 1]
# Create a new sorted list without modifying the original
original = [3, 1, 4, 1, 5, 9, 2, 6]
sorted_numbers = sorted(original)
print(sorted_numbers) # [1, 1, 2, 3, 4, 5, 6, 9]
print(original) # [3, 1, 4, 1, 5, 9, 2, 6] (unchanged)
# Sort strings (alphabetically)
fruits = ["banana", "cherry", "apple", "date"]
fruits.sort()
print(fruits) # ["apple", "banana", "cherry", "date"]
# Custom sorting with key parameter
students = [
{"name": "Alice", "grade": 88},
{"name": "Bob", "grade": 75},
{"name": "Charlie", "grade": 93}
]
# Sort by grade
students.sort(key=lambda student: student["grade"])
print([student["name"] for student in students]) # ["Bob", "Alice", "Charlie"]
Other Useful Methods
# Reverse the list in place
fruits = ["apple", "banana", "cherry"]
fruits.reverse()
print(fruits) # ["cherry", "banana", "apple"]
# Copy a list
fruits = ["apple", "banana", "cherry"]
fruits_copy = fruits.copy() # or list(fruits) or fruits[:]
fruits_copy.append("date")
print(fruits) # ["apple", "banana", "cherry"]
print(fruits_copy) # ["apple", "banana", "cherry", "date"]
List Comprehensions
List comprehensions provide a concise way to create lists:
# Create a list of squares
squares = [x**2 for x in range(1, 6)]
print(squares) # [1, 4, 9, 16, 25]
# With a condition
even_squares = [x**2 for x in range(1, 11) if x % 2 == 0]
print(even_squares) # [4, 16, 36, 64, 100]
# With multiple conditions
numbers = [x for x in range(1, 31) if x % 2 == 0 if x % 3 == 0]
print(numbers) # [6, 12, 18, 24, 30]
# Nested loops in comprehension
pairs = [(x, y) for x in range(1, 3) for y in range(1, 3)]
print(pairs) # [(1, 1), (1, 2), (2, 1), (2, 2)]
# Creating a flat list from a nested list
nested = [[1, 2], [3, 4], [5, 6]]
flat = [item for sublist in nested for item in sublist]
print(flat) # [1, 2, 3, 4, 5, 6]
Lists vs. Other Data Structures
Understanding when to use lists versus other data structures is important:
Data Structure | Ordered | Mutable | Duplicates | Use Case |
---|---|---|---|---|
List | Yes | Yes | Yes | General purpose, when order matters and items might change |
Tuple | Yes | No | Yes | Immutable sequences, like coordinates or database records |
Set | No | Yes | No | When you need unique items or set operations |
Dictionary | Yes* | Yes | No (keys) | Key-value mapping, like a phone book |
*Dictionaries maintained insertion order starting in Python 3.7
# Example: Converting between data structures
numbers_list = [1, 2, 3, 2, 1, 4]
numbers_tuple = tuple(numbers_list) # (1, 2, 3, 2, 1, 4)
numbers_set = set(numbers_list) # {1, 2, 3, 4}
numbers_dict = {i: numbers_list[i] for i in range(len(numbers_list))} # {0: 1, 1: 2, 2: 3, 3: 2, 4: 1, 5: 4}
Practical Examples
Example 1: Todo List Manager
def todo_list_manager():
"""A simple todo list manager using lists."""
todos = []
while True:
print("\nTodo List Manager")
print("1. Add task")
print("2. View tasks")
print("3. Mark task as done")
print("4. Remove task")
print("5. Exit")
choice = input("Enter your choice (1-5): ")
if choice == "1":
task = input("Enter a new task: ")
todos.append({"task": task, "done": False})
print(f"Task '{task}' added.")
elif choice == "2":
if not todos:
print("No tasks in the list.")
else:
print("\nYour Tasks:")
for i, item in enumerate(todos):
status = "✓" if item["done"] else " "
print(f"{i+1}. [{status}] {item['task']}")
elif choice == "3":
if not todos:
print("No tasks to mark as done.")
else:
try:
task_num = int(input("Enter task number to mark as done: ")) - 1
if 0 <= task_num < len(todos):
todos[task_num]["done"] = True
print(f"Task '{todos[task_num]['task']}' marked as done.")
else:
print("Invalid task number.")
except ValueError:
print("Please enter a valid number.")
elif choice == "4":
if not todos:
print("No tasks to remove.")
else:
try:
task_num = int(input("Enter task number to remove: ")) - 1
if 0 <= task_num < len(todos):
removed_task = todos.pop(task_num)
print(f"Task '{removed_task['task']}' removed.")
else:
print("Invalid task number.")
except ValueError:
print("Please enter a valid number.")
elif choice == "5":
print("Goodbye!")
break
else:
print("Invalid choice. Please enter a number between 1 and 5.")
# Uncomment to run the todo list manager
# todo_list_manager()
Example 2: Basic Data Analysis
def analyze_temperatures(daily_temperatures):
"""
Analyze a list of daily temperatures and return statistics.
Args:
daily_temperatures: A list of daily temperature readings
Returns:
A dictionary containing various statistics
"""
if not daily_temperatures:
return {"error": "No temperature data provided"}
# Calculate statistics
average_temp = sum(daily_temperatures) / len(daily_temperatures)
min_temp = min(daily_temperatures)
max_temp = max(daily_temperatures)
# Find temperature range
temp_range = max_temp - min_temp
# Count days above average
days_above_avg = sum(1 for temp in daily_temperatures if temp > average_temp)
# Temperature trend (increasing or decreasing)
increasing_days = 0
for i in range(1, len(daily_temperatures)):
if daily_temperatures[i] > daily_temperatures[i-1]:
increasing_days += 1
trend_percentage = (increasing_days / (len(daily_temperatures) - 1)) * 100
trend = "Increasing" if trend_percentage > 50 else "Decreasing"
return {
"average": round(average_temp, 1),
"minimum": min_temp,
"maximum": max_temp,
"range": temp_range,
"days_above_average": days_above_avg,
"trend": trend
}
# Sample usage
temperatures = [68, 71, 70, 75, 74, 72, 77, 78, 74]
analysis = analyze_temperatures(temperatures)
print("Temperature Analysis:")
for key, value in analysis.items():
print(f"{key.replace('_', ' ').title()}: {value}")
Example 3: Matrix Operations
def print_matrix(matrix):
"""Print a matrix in a readable format."""
for row in matrix:
print(" ".join(str(element) for element in row))
def matrix_addition(matrix_a, matrix_b):
"""Add two matrices of the same dimensions."""
if len(matrix_a) != len(matrix_b) or len(matrix_a[0]) != len(matrix_b[0]):
raise ValueError("Matrices must have the same dimensions")
result = []
for i in range(len(matrix_a)):
row = []
for j in range(len(matrix_a[0])):
row.append(matrix_a[i][j] + matrix_b[i][j])
result.append(row)
return result
def matrix_transpose(matrix):
"""Calculate the transpose of a matrix."""
# Initialize result matrix with zeros
rows = len(matrix)
cols = len(matrix[0])
result = [[0 for _ in range(rows)] for _ in range(cols)]
# Fill in transposed values
for i in range(rows):
for j in range(cols):
result[j][i] = matrix[i][j]
return result
# Example matrices
matrix_a = [
[1, 2, 3],
[4, 5, 6]
]
matrix_b = [
[7, 8, 9],
[10, 11, 12]
]
print("Matrix A:")
print_matrix(matrix_a)
print("\nMatrix B:")
print_matrix(matrix_b)
print("\nMatrix A + B:")
sum_matrix = matrix_addition(matrix_a, matrix_b)
print_matrix(sum_matrix)
print("\nTranspose of Matrix A:")
transpose_a = matrix_transpose(matrix_a)
print_matrix(transpose_a)
Common List Operations and Their Time Complexity
Understanding the time complexity of list operations helps you write more efficient code:
Operation | Example | Time Complexity | Description |
---|---|---|---|
Indexing | lst[i] | O(1) | Access an element by index |
Assignment | lst[i] = x | O(1) | Assign a value to an index |
Append | lst.append(x) | O(1) | Add an element to the end |
Pop (end) | lst.pop() | O(1) | Remove the last element |
Pop (middle) | lst.pop(i) | O(n) | Remove element at index i |
Insert | lst.insert(i, x) | O(n) | Insert element at index i |
Delete | del lst[i] | O(n) | Delete element at index i |
Remove | lst.remove(x) | O(n) | Remove first occurrence of x |
Containment | x in lst | O(n) | Check if x is in the list |
Iteration | for x in lst | O(n) | Iterate through list |
Get Length | len(lst) | O(1) | Get number of elements |
Slicing | lst[a:b] | O(b-a) | Get a slice of the list |
Extend | lst.extend(lst2) | O(len(lst2)) | Add all elements of lst2 |
Sort | lst.sort() | O(n log n) | Sort the list in place |
Multiply | lst * n | O(n*len(lst)) | Create a list with n copies |
Note: Some operations that seem simple can be inefficient for large lists. For example, repeatedly appending to a list is efficient, but repeatedly inserting at the beginning is not, as it requires shifting all other elements.
Common Patterns and Techniques
Pattern 1: Filtering
# Filter even numbers
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
even_numbers = [num for num in numbers if num % 2 == 0]
print(even_numbers) # [2, 4, 6, 8, 10]
# Filter with a function
def is_premium_member(customer):
return customer["membership"] == "premium"
customers = [
{"name": "Alice", "membership": "premium"},
{"name": "Bob", "membership": "standard"},
{"name": "Charlie", "membership": "premium"}
]
premium_customers = list(filter(is_premium_member, customers))
print([customer["name"] for customer in premium_customers]) # ["Alice", "Charlie"]
Pattern 2: Mapping
# Transform each element
numbers = [1, 2, 3, 4, 5]
squared = [num ** 2 for num in numbers]
print(squared) # [1, 4, 9, 16, 25]
# Map with a function
def celsius_to_fahrenheit(celsius):
return (celsius * 9/5) + 32
celsius_temps = [0, 10, 20, 30, 40]
fahrenheit_temps = list(map(celsius_to_fahrenheit, celsius_temps))
print(fahrenheit_temps) # [32.0, 50.0, 68.0, 86.0, 104.0]
Pattern 3: Finding Items
def find_first(items, predicate):
"""Find the first item that matches the predicate."""
for item in items:
if predicate(item):
return item
return None
numbers = [1, 3, 5, 8, 10, 12]
first_even = find_first(numbers, lambda x: x % 2 == 0)
print(first_even) # 8
# Using next() and generator expressions
first_even_alt = next((x for x in numbers if x % 2 == 0), None)
print(first_even_alt) # 8
Pattern 4: Grouping
def group_by_category(items, key_func):
"""Group items by a category determined by key_func."""
result = {}
for item in items:
key = key_func(item)
if key not in result:
result[key] = []
result[key].append(item)
return result
products = [
{"name": "Apples", "category": "Fruit", "price": 1.50},
{"name": "Bread", "category": "Bakery", "price": 2.50},
{"name": "Carrots", "category": "Vegetable", "price": 1.00},
{"name": "Bananas", "category": "Fruit", "price": 0.75}
]
# Group by category
grouped = group_by_category(products, lambda x: x["category"])
for category, items in grouped.items():
print(f"{category}: {[item['name'] for item in items]}")
Pattern 5: Zipping Lists
names = ["Alice", "Bob", "Charlie"]
ages = [25, 30, 35]
occupations = ["Engineer", "Doctor", "Teacher"]
# Combine related data from multiple lists
people = list(zip(names, ages, occupations))
print(people) # [("Alice", 25, "Engineer"), ("Bob", 30, "Doctor"), ("Charlie", 35, "Teacher")]
# Create a list of dictionaries
people_dicts = [{"name": n, "age": a, "occupation": o} for n, a, o in zip(names, ages, occupations)]
print(people_dicts)
Common Pitfalls and Best Practices
Pitfall 1: Modifying a List While Iterating
# Incorrect - modifying the list during iteration
numbers = [1, 2, 3, 4, 5]
for number in numbers:
if number % 2 == 0:
numbers.remove(number) # This can cause unexpected results
# Correct - create a new list or iterate over a copy
numbers = [1, 2, 3, 4, 5]
numbers = [num for num in numbers if num % 2 != 0]
# or
numbers = [1, 2, 3, 4, 5]
for number in numbers[:]: # Iterate over a copy
if number % 2 == 0:
numbers.remove(number)
Pitfall 2: Shallow vs. Deep Copying
# Original list with nested lists
original = [1, 2, [3, 4]]
# Shallow copy - nested lists are still shared
shallow_copy = original.copy()
# Deep copy - completely independent
import copy
deep_copy = copy.deepcopy(original)
# Modify the nested list in the original
original[2][0] = 99
print(original) # [1, 2, [99, 4]]
print(shallow_copy) # [1, 2, [99, 4]] - nested list was affected
print(deep_copy) # [1, 2, [3, 4]] - completely independent
Pitfall 3: Lists as Default Parameters
# Incorrect - the default list is created once and shared
def add_to_list(item, my_list=[]):
my_list.append(item)
return my_list
print(add_to_list("a")) # ["a"]
print(add_to_list("b")) # ["a", "b"] - Not a new empty list!
# Correct - use None as default and create a new list inside
def add_to_list_fixed(item, my_list=None):
if my_list is None:
my_list = []
my_list.append(item)
return my_list
print(add_to_list_fixed("a")) # ["a"]
print(add_to_list_fixed("b")) # ["b"]
Best Practice 1: Use List Comprehensions for Clarity
# Less readable loop
squares = []
for i in range(10):
if i % 2 == 0:
squares.append(i ** 2)
# More readable list comprehension
squares = [i ** 2 for i in range(10) if i % 2 == 0]
Best Practice 2: Use Appropriate Built-in Functions
numbers = [3, 1, 4, 1, 5, 9, 2, 6, 5]
# Directly use built-ins for common operations
total = sum(numbers)
largest = max(numbers)
smallest = min(numbers)
exists = 5 in numbers
position = numbers.index(5)
Best Practice 3: Choose the Right Tool
# If you need unique elements, use a set
unique_numbers = list(set([3, 1, 4, 1, 5, 9, 2, 6, 5]))
# If you need key-value pairs, use a dictionary
country_codes = dict(zip(["USA", "Canada", "Mexico"], [1, 2, 3]))
# If data won't change, use a tuple
coordinates = (40.7128, -74.0060)
Exercises
Exercise 1: Write a function that takes a list of numbers and returns a new list with only the even numbers. If there are no even numbers, return an empty list.
Exercise 2: Create a function called remove_duplicates
that takes a list and returns a new list with all duplicate elements removed while preserving the original order. Do not use sets for this exercise (to practice list operations).
Exercise 3: Write a function called flatten_list
that takes a nested list (a list that contains lists) and returns a flattened version with all elements in a single level list.
Exercise 4: Implement a function called rotate_list
that takes a list and a number n
. The function should rotate the list elements by n
positions. If n
is positive, rotate to the right, if negative, rotate to the left.
Hint for Exercise 1: Use a list comprehension with a condition to check if each number is even.
def get_even_numbers(numbers):
return [num for num in numbers if num % 2 == 0]
In the next section, we’ll explore tuples, which are similar to lists but have some important differences, particularly their immutability.