Python Lists: Methods, Mutability, and the Patterns You’ll Use Every Day
Lists are Python’s most versatile general-purpose container. They’re ordered, mutable, and can hold any mix of types. Understanding how lists work — not just what methods exist, but how memory and mutation behave — prevents a whole category of bugs that confuse developers at every experience level.
What a List Is
A list is an ordered, mutable sequence enclosed in square brackets. The items can be any Python object, and a single list can mix types:
numbers = [1, 2, 3, 4, 5]mixed = [42, "hello", True, None, [1, 2]] # nesting worksempty = []Unlike strings, lists are mutable — you can change their contents in place without creating a new object.
Creating Lists
# Direct literalfruits = ["apple", "banana", "cherry"]
# From another iterableletters = list("Python") # ['P', 'y', 't', 'h', 'o', 'n']range_list = list(range(5)) # [0, 1, 2, 3, 4]
# Repeated patternzeros = [0] * 5 # [0, 0, 0, 0, 0]
# List comprehensionsquares = [x**2 for x in range(6)] # [0, 1, 4, 9, 16, 25]Indexing and Slicing
Lists use zero-based indexing. Negative indices count from the end. Slicing returns a new list:
items = ["a", "b", "c", "d", "e"]
print(items[0]) # "a"print(items[-1]) # "e"print(items[1:4]) # ["b", "c", "d"]print(items[::2]) # ["a", "c", "e"] — every other itemprint(items[::-1]) # ["e", "d", "c", "b", "a"] — reversedModifying Lists
colours = ["red", "green", "blue"]
# Change by indexcolours[1] = "yellow" # ["red", "yellow", "blue"]
# Add itemscolours.append("purple") # add to endcolours.insert(0, "black") # insert at position 0colours.extend(["white", "grey"]) # add multiple items
# Remove itemscolours.remove("red") # remove first occurrencedel colours[0] # remove by indexlast = colours.pop() # remove and return last itemsecond = colours.pop(1) # remove and return item at index 1
# Clear all itemscolours.clear()Sorting
Lists have an in-place sort() method and there’s a built-in sorted() that returns a new list:
nums = [3, 1, 4, 1, 5, 9, 2, 6]
nums.sort() # modifies nums in placeprint(nums) # [1, 1, 2, 3, 4, 5, 6, 9]
nums.sort(reverse=True) # descendingprint(nums) # [9, 6, 5, 4, 3, 2, 1, 1]
# sorted() returns a new list, leaves original unchangedoriginal = [3, 1, 4, 1, 5]ascending = sorted(original)print(original) # [3, 1, 4, 1, 5] — unchangedprint(ascending) # [1, 1, 3, 4, 5]Sorting by a key
Both sort() and sorted() accept a key parameter — a function applied to each element before comparing:
words = ["banana", "apple", "cherry", "fig"]
# Sort by string lengthwords.sort(key=len)print(words) # ['fig', 'apple', 'banana', 'cherry']
# Sort by last characterwords.sort(key=lambda w: w[-1])
# Sort a list of dicts by a fieldstudents = [ {"name": "Alice", "score": 88}, {"name": "Bob", "score": 95}, {"name": "Charlie", "score": 72},]students.sort(key=lambda s: s["score"], reverse=True)# Bob (95), Alice (88), Charlie (72)Copying Lists — The Mutation Trap
This is the most common beginner trap with lists. Assignment does not copy a list — it creates a second reference to the same object:
original = [1, 2, 3]alias = original # both point to the same list
alias.append(4)print(original) # [1, 2, 3, 4] — original was modified!To get an independent copy:
# Shallow copy — three equivalent wayscopy1 = original.copy()copy2 = original[:]copy3 = list(original)
copy1.append(99)print(original) # [1, 2, 3, 4] — unchangedA shallow copy works when the list contains only simple values (numbers, strings, booleans). If the list contains nested mutable objects, you need a deep copy:
import copy
matrix = [[1, 2], [3, 4]]shallow = matrix.copy()deep = copy.deepcopy(matrix)
shallow[0].append(99) # modifies matrix[0] too — same inner listprint(matrix) # [[1, 2, 99], [3, 4]]
# deep is fully independentdeep[0].append(0)print(matrix) # still [[1, 2, 99], [3, 4]]Other Useful Methods
nums = [3, 1, 4, 1, 5, 1, 9]
print(nums.count(1)) # 3 — how many times 1 appearsprint(nums.index(5)) # 4 — index of first 5nums.reverse() # reverse in place
# Check membershipprint(4 in nums) # Trueprint(7 not in nums) # True
# Length, min, max, sumprint(len(nums)) # 7print(min(nums)) # 1print(max(nums)) # 9print(sum(nums)) # 24Common Patterns
Filtering a list
scores = [45, 82, 67, 90, 55, 78]
# Keep scores above 70passing = [s for s in scores if s >= 70] # [82, 90, 78]
# Filter with built-in filter()passing = list(filter(lambda s: s >= 70, scores))Transforming a list
names = ["alice", "bob", "charlie"]capitalised = [name.title() for name in names] # ["Alice", "Bob", "Charlie"]Flattening nested lists
nested = [[1, 2, 3], [4, 5], [6]]flat = [item for sublist in nested for item in sublist]# [1, 2, 3, 4, 5, 6]Removing duplicates while preserving order
items = [3, 1, 4, 1, 5, 9, 2, 6, 5]seen = set()unique = []for item in items: if item not in seen: seen.add(item) unique.append(item)# [3, 1, 4, 5, 9, 2, 6]Or in Python 3.7+ using dict to preserve insertion order:
unique = list(dict.fromkeys(items))When Not to Use a List
Lists work for most use cases, but consider alternatives when:
- You need fast membership testing with large data → use a
set - You need key-value lookup → use a
dict - You need thread-safe queue operations → use
collections.deque - The data shouldn’t change → use a
tuple - You’re doing heavy numerical computation → use a
numpyarray