Python Lists as a Collection: Mutability, Nesting, and Common Operations
A Python list is an ordered, mutable collection that can hold any mix of types. It is the most commonly used data structure in Python โ and also one that gets misused when developers reach for it by habit rather than by choice.
Creating Lists
# Direct literalfruits = ["apple", "banana", "cherry"]
# List constructor from any iterablenumbers = list(range(1, 6)) # [1, 2, 3, 4, 5]chars = list("Python") # ['P', 'y', 't', 'h', 'o', 'n']from_tuple = list((10, 20, 30))
# Repeat a patternzeros = [0] * 5 # [0, 0, 0, 0, 0]
# List comprehensionsquares = [x**2 for x in range(1, 6)] # [1, 4, 9, 16, 25]evens = [x for x in range(20) if x % 2 == 0]Mutable Operations
Lists support in-place modification. This is their primary advantage over tuples.
items = [3, 1, 4, 1, 5, 9]
# Add elementsitems.append(2) # adds to end: [3, 1, 4, 1, 5, 9, 2]items.insert(0, 0) # insert at index 0: [0, 3, 1, 4, 1, 5, 9, 2]items.extend([6, 5, 3]) # add multiple: [..., 6, 5, 3]
# Remove elementsitems.remove(1) # removes first occurrence of 1popped = items.pop() # removes and returns last elementpopped_at = items.pop(2) # removes and returns element at index 2del items[0] # removes element at index 0
# Find and countprint(items.index(5)) # index of first occurrence of 5print(items.count(3)) # how many times 3 appearssort() vs sorted() โ Knowing the Difference
This is a common source of bugs:
numbers = [5, 2, 9, 1, 7, 3]
# sort() modifies the list IN PLACE and returns Nonenumbers.sort()print(numbers) # [1, 2, 3, 5, 7, 9]
# sorted() returns a NEW sorted list โ original unchangedoriginal = [5, 2, 9, 1, 7, 3]new_sorted = sorted(original)print(original) # [5, 2, 9, 1, 7, 3] โ unchangedprint(new_sorted) # [1, 2, 3, 5, 7, 9]
# Sort by a keywords = ["banana", "apple", "fig", "cherry"]words.sort(key=len) # sort by string lengthprint(words) # ['fig', 'apple', 'banana', 'cherry']
# Reverse sortnumbers.sort(reverse=True)print(numbers) # [9, 7, 5, 3, 2, 1]
# Common mistake: assigning sort() to a variableresult = numbers.sort() # result is None!Nested Lists (Matrices and Tables)
# 3ร3 matrixmatrix = [ [1, 2, 3], [4, 5, 6], [7, 8, 9],]
# Access element at row 1, column 2print(matrix[1][2]) # 6
# Transpose: swap rows and columnstransposed = [[matrix[j][i] for j in range(3)] for i in range(3)]print(transposed)# [[1, 4, 7], [2, 5, 8], [3, 6, 9]]
# Warning: avoid list multiplication for nested listswrong = [[0] * 3] * 3 # all three rows are the SAME objectwrong[0][0] = 99print(wrong) # [[99, 0, 0], [99, 0, 0], [99, 0, 0]] โ unexpected!
correct = [[0] * 3 for _ in range(3)] # each row is a separate listcorrect[0][0] = 99print(correct) # [[99, 0, 0], [0, 0, 0], [0, 0, 0]]The [[0] * 3] * 3 trap is one of the most common Python bugs with nested lists.
List Comprehension vs map()
Both transform a list โ comprehensions are generally preferred for readability:
numbers = [1, 2, 3, 4, 5]
# List comprehensionsquared = [x**2 for x in numbers]
# map() โ functional style, returns an iteratorsquared_map = list(map(lambda x: x**2, numbers))
# Both produce [1, 4, 9, 16, 25]# Comprehension is more Pythonic for simple transforms
# map() can be useful when the function already existsimport mathroots = list(map(math.sqrt, numbers)) # cleaner than [math.sqrt(x) for x in ...]
# With filterevens = [x for x in numbers if x % 2 == 0]evens_filter = list(filter(lambda x: x % 2 == 0, numbers))Useful Built-in Functions with Lists
data = [10, 50, 75, 83, 98, 84, 32, 10]
print(min(data)) # 10print(max(data)) # 98print(sum(data)) # 442print(len(data)) # 8print(sum(data) / len(data)) # 55.25 (mean)
# enumerate โ iterate with indexfor i, value in enumerate(data, start=1): print(f"{i}: {value}")
# zip โ iterate multiple lists togethernames = ["Alice", "Bob", "Carol"]scores = [85, 92, 78]for name, score in zip(names, scores): print(f"{name}: {score}")
# any and allprint(any(x > 90 for x in scores)) # True โ at least oneprint(all(x >= 70 for x in scores)) # True โ all passMemory Consideration: Lists Grow Dynamically
Python lists over-allocate memory to amortise the cost of append(). When you create a list with a known final size, allocating it up front (via comprehension or [None] * n) is slightly more efficient than repeated append() calls:
import sys
# Growing list via appendlst = []for i in range(10): lst.append(i) print(f"len={len(lst)}, size={sys.getsizeof(lst)} bytes")
# Pre-allocated via comprehension โ consistent memory from the startlst2 = [i for i in range(10)]For large lists in performance-critical code, this matters. For most application code, append() is fine.