Python - List Tutorial (Complete Guide)
• Python lists are mutable, ordered sequences that can contain mixed data types and support powerful operations like slicing, comprehension, and in-place modification
Key Insights
• Python lists are mutable, ordered sequences that can contain mixed data types and support powerful operations like slicing, comprehension, and in-place modification • List performance characteristics matter—appending is O(1), inserting at arbitrary positions is O(n), and understanding these trade-offs prevents bottlenecks in production code • Modern Python offers list comprehensions, unpacking operators, and built-in methods that eliminate the need for verbose loops and make code more Pythonic
Creating and Initializing Lists
Lists in Python use square bracket notation and accept heterogeneous elements:
# Basic initialization
empty_list = []
numbers = [1, 2, 3, 4, 5]
mixed = [1, "string", 3.14, True, None]
# List constructor
from_range = list(range(5)) # [0, 1, 2, 3, 4]
from_string = list("abc") # ['a', 'b', 'c']
# List comprehension
squares = [x**2 for x in range(10)]
evens = [x for x in range(20) if x % 2 == 0]
# Pre-allocated lists
zeros = [0] * 5 # [0, 0, 0, 0, 0]
matrix = [[0] * 3 for _ in range(3)] # Correct 3x3 matrix
Critical gotcha: Never use [[0] * 3] * 3 for matrices. This creates references to the same inner list, causing mutations to affect all rows.
Accessing and Slicing
Python provides zero-based indexing with negative indices for reverse access:
data = [10, 20, 30, 40, 50]
# Basic indexing
first = data[0] # 10
last = data[-1] # 50
second_last = data[-2] # 40
# Slicing: list[start:stop:step]
first_three = data[:3] # [10, 20, 30]
last_two = data[-2:] # [40, 50]
middle = data[1:4] # [20, 30, 40]
every_second = data[::2] # [10, 30, 50]
reversed_list = data[::-1] # [50, 40, 30, 20, 10]
# Slice assignment
data[1:3] = [200, 300] # [10, 200, 300, 40, 50]
data[::2] = [1, 2, 3] # Replace every second element
Slicing creates shallow copies. For nested structures, use copy.deepcopy().
Modifying Lists
Lists are mutable, supporting in-place operations:
items = [1, 2, 3]
# Append and extend
items.append(4) # [1, 2, 3, 4]
items.extend([5, 6]) # [1, 2, 3, 4, 5, 6]
items += [7, 8] # Same as extend
# Insert and remove
items.insert(0, 0) # [0, 1, 2, 3, 4, 5, 6, 7, 8]
items.remove(0) # Removes first occurrence of 0
popped = items.pop() # Removes and returns last element
popped_index = items.pop(0) # Removes and returns element at index 0
# Clear
items.clear() # []
# Replace elements
items = [1, 2, 3, 4, 5]
items[2] = 99 # [1, 2, 99, 4, 5]
Performance note: append() is O(1) amortized, but insert(0, x) is O(n). For frequent insertions at the beginning, use collections.deque.
Searching and Sorting
numbers = [3, 1, 4, 1, 5, 9, 2, 6]
# Searching
index = numbers.index(4) # Returns 2
count = numbers.count(1) # Returns 2
exists = 5 in numbers # True
# Sorting (in-place)
numbers.sort() # Modifies original: [1, 1, 2, 3, 4, 5, 6, 9]
numbers.sort(reverse=True) # Descending order
# Sorting (new list)
original = [3, 1, 4, 1, 5]
sorted_copy = sorted(original) # original unchanged
# Custom sorting
words = ["apple", "pie", "a", "cherry"]
words.sort(key=len) # Sort by length
words.sort(key=lambda x: x[-1]) # Sort by last character
# Complex sorting
users = [
{"name": "Alice", "age": 30},
{"name": "Bob", "age": 25},
{"name": "Charlie", "age": 35}
]
users.sort(key=lambda u: u["age"])
List Comprehensions
List comprehensions provide concise syntax for transformations and filtering:
# Basic transformation
squares = [x**2 for x in range(10)]
# With conditional
evens = [x for x in range(20) if x % 2 == 0]
# Multiple conditions
filtered = [x for x in range(100) if x % 2 == 0 if x % 5 == 0]
# Nested loops
pairs = [(x, y) for x in range(3) for y in range(3)]
# [(0,0), (0,1), (0,2), (1,0), (1,1), (1,2), (2,0), (2,1), (2,2)]
# Flattening
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flattened = [num for row in matrix for num in row]
# With if-else (note syntax difference)
processed = [x if x > 0 else 0 for x in [-2, -1, 0, 1, 2]]
# [0, 0, 0, 1, 2]
Advanced Operations
# Unpacking
first, *middle, last = [1, 2, 3, 4, 5]
# first=1, middle=[2, 3, 4], last=5
# Merging lists
list1 = [1, 2, 3]
list2 = [4, 5, 6]
merged = [*list1, *list2] # [1, 2, 3, 4, 5, 6]
# Zip for parallel iteration
names = ["Alice", "Bob", "Charlie"]
ages = [25, 30, 35]
combined = list(zip(names, ages))
# [('Alice', 25), ('Bob', 30), ('Charlie', 35)]
# Enumerate for index tracking
for index, value in enumerate(["a", "b", "c"]):
print(f"{index}: {value}")
# Filter and map
numbers = [1, 2, 3, 4, 5, 6]
evens = list(filter(lambda x: x % 2 == 0, numbers))
doubled = list(map(lambda x: x * 2, numbers))
# Any and all
has_even = any(x % 2 == 0 for x in numbers)
all_positive = all(x > 0 for x in numbers)
Common Patterns
Removing duplicates while preserving order:
def remove_duplicates(items):
seen = set()
result = []
for item in items:
if item not in seen:
seen.add(item)
result.append(item)
return result
# Or using dict (Python 3.7+)
unique = list(dict.fromkeys([1, 2, 2, 3, 1, 4]))
Chunking lists:
def chunk_list(items, chunk_size):
return [items[i:i + chunk_size] for i in range(0, len(items), chunk_size)]
data = list(range(10))
chunks = chunk_list(data, 3) # [[0,1,2], [3,4,5], [6,7,8], [9]]
Finding indices of all occurrences:
def find_all_indices(items, target):
return [i for i, x in enumerate(items) if x == target]
numbers = [1, 2, 3, 2, 4, 2, 5]
indices = find_all_indices(numbers, 2) # [1, 3, 5]
Performance Considerations
import timeit
# Appending: O(1) amortized
def append_test():
result = []
for i in range(10000):
result.append(i)
return result
# Inserting at beginning: O(n) per operation
def insert_test():
result = []
for i in range(1000): # Reduced size due to O(n²) complexity
result.insert(0, i)
return result
# List comprehension vs loop
def comprehension_test():
return [x**2 for x in range(10000)]
def loop_test():
result = []
for x in range(10000):
result.append(x**2)
return result
List comprehensions are typically 10-20% faster than equivalent loops due to optimized C implementation.
Memory Management
Lists over-allocate to minimize reallocations during growth:
import sys
# Memory growth pattern
sizes = []
for i in range(20):
lst = [0] * i
sizes.append(sys.getsizeof(lst))
# Lists allocate extra capacity
# Growth pattern: 0, 4, 8, 16, 25, 35, 46, 58, 72, 88...
For memory-critical applications with large datasets, consider array.array for homogeneous numeric data or generators for lazy evaluation.
Best Practices
Use list comprehensions for simple transformations, but break complex logic into regular loops for readability. Avoid modifying lists during iteration—create new lists or iterate over copies. Choose appropriate data structures: lists for ordered sequences with frequent appends, sets for membership testing, deques for frequent insertions/deletions at both ends.