Python List Methods
A Python list is not just a container — it comes with a built-in toolkit of methods that let you add, remove, search, count, sort, and copy its contents. Because lists are mutable, most of these methods change the list directly instead of creating a new one. Understanding exactly which methods mutate in place, which return a value, and which do both is one of the most important steps toward writing correct, bug-free Python.
Overview: How List Methods Work
Every list you create is an object living somewhere in memory. A variable like fruits doesn’t contain the list itself — it holds a reference (a pointer) to that object. When you call a method such as fruits.append("kiwi"), Python looks up the append method on the list object referenced by fruits and modifies that object’s internal array of pointers directly. No new list is created, and any other variable pointing at the same object will “see” the change too.
This matters because list methods split into two very different families:
- Mutating methods (
append,extend,insert,remove,pop,clear,sort,reverse) change the list in place. Most of them returnNone— Python’s convention is that a function either mutates something or returns a new value, rarely both. - Non-mutating / query methods (
index,count,copy) do not change the original list.indexandcountreturn information about the list, andcopyreturns a brand-new list object.
Internally, CPython implements a list as a dynamically resizing array of pointers to objects (not a linked list). That’s why indexing (my_list[i]) is very fast (O(1)), appending is usually fast (amortized O(1), since CPython over-allocates extra room), but inserting or removing near the front of a large list is slow (O(n)) because every following element has to shift.
Syntax
The general form for calling any list method is:
list_name.method_name(arguments)
Here is a reference table of the most commonly used list methods:
| Method | Description | Returns |
|---|---|---|
append(x) |
Adds x as a single new item at the end |
None |
extend(iterable) |
Adds every item from iterable to the end |
None |
insert(i, x) |
Inserts x before index i |
None |
remove(x) |
Removes the first item equal to x (raises ValueError if absent) |
None |
pop(i=-1) |
Removes and returns the item at index i (default: last item) |
The removed item |
clear() |
Removes every item from the list | None |
index(x) |
Returns the index of the first item equal to x |
int |
count(x) |
Returns how many times x appears |
int |
sort(key=None, reverse=False) |
Sorts the list in place | None |
reverse() |
Reverses the list in place | None |
copy() |
Returns a shallow copy of the list | A new list |
Note the difference between the sort() method (in place, returns None) and the sorted() built-in function, which takes any iterable and returns a brand-new sorted list, leaving the original untouched.
Examples
Example 1: The core mutating methods
fruits = ["apple", "banana", "cherry"]
fruits.append("date")
fruits.insert(1, "blueberry")
fruits.remove("banana")
print(fruits)
fruits.sort()
print(fruits)
fruits.reverse()
print(fruits)
last = fruits.pop()
print(last)
print(fruits)
Output:
['apple', 'blueberry', 'cherry', 'date']
['apple', 'blueberry', 'cherry', 'date']
['date', 'cherry', 'blueberry', 'apple']
apple
['date', 'cherry', 'blueberry']
Each call mutates fruits directly. append adds to the end, insert(1, ...) shifts everything from index 1 onward to make room, and remove("banana") searches by value and deletes the first match. sort() happens to leave the list unchanged here because it was already alphabetical, reverse() flips the order, and pop() removes and returns the final element, shrinking the list to three items.
Example 2: A realistic case — ranking students by score
students = [
{"name": "Maya", "score": 88},
{"name": "Liam", "score": 95},
{"name": "Zoe", "score": 72},
]
new_students = [{"name": "Ravi", "score": 91}]
students.extend(new_students)
students.sort(key=lambda s: s["score"], reverse=True)
for rank, student in enumerate(students, start=1):
print(f"{rank}. {student['name']} - {student['score']}")
scores = [s["score"] for s in students]
print("Highest score index:", scores.index(max(scores)))
print("Number of students:", len(students))
Output:
1. Liam - 95
2. Ravi - 91
3. Maya - 88
4. Zoe - 72
Highest score index: 0
Number of students: 4
extend() merges the new one-item list into students without creating a nested list (unlike append, which would have added the whole list as a single element). The key=lambda s: s["score"] argument tells sort() exactly what value to compare for each dictionary, and reverse=True sorts from highest to lowest. Finally, index() finds where the top score lives in the now-sorted list.
Example 3: References vs. copies, and using a list as a stack
original = [1, 2, 3]
alias = original
shallow_copy = original.copy()
alias.append(4)
shallow_copy.append(99)
print("original:", original)
print("alias:", alias)
print("shallow_copy:", shallow_copy)
stack = []
stack.append("task1")
stack.append("task2")
stack.append("task3")
while stack:
current = stack.pop()
print("Processing:", current)
Output:
original: [1, 2, 3, 4]
alias: [1, 2, 3, 4]
shallow_copy: [1, 2, 3, 99]
Processing: task3
Processing: task2
Processing: task1
alias = original does not copy anything — both names point at the same list object, so mutating alias also changes what original shows. original.copy(), by contrast, creates an independent list, so changes to shallow_copy don’t affect original. The second half shows the classic “stack” pattern: repeatedly calling append() to push and pop() (with no argument) to pop from the end gives you last-in-first-out behavior, which is why the tasks print in reverse order.
Under the Hood: In-Place Mutation and Return Values
When you write numbers.sort(), Python calls CPython’s built-in Timsort algorithm directly on the array backing the list, rearranging the existing pointers — no new list object is allocated for the result. Timsort is a stable sort, meaning items that compare as equal keep their original relative order, which is important when sorting by one key (like score) while wanting ties broken by original position.
remove(x) and index(x) both perform a linear scan from the start of the list, comparing each element to x with == until a match is found. If no match exists, both raise a ValueError. This is different from pop(i), which works by position and raises an IndexError if the index is out of range.
copy() (and the equivalent slice my_list[:]) performs a shallow copy: it creates a new list object, but if the elements themselves are mutable (like nested lists or dictionaries), the new list still holds references to the very same inner objects. To fully duplicate a list of mutable objects, use copy.deepcopy() from the standard library.
Common Mistakes
Mistake 1: Assuming sort() returns the sorted list
numbers = [3, 1, 2]
numbers = numbers.sort()
print(numbers)
Output:
None
sort() mutates numbers in place and returns None, so reassigning numbers = numbers.sort() throws away the list and replaces it with None. Fix it by either calling sort() on its own line, or using sorted() if you want a new list:
numbers = [3, 1, 2]
numbers.sort()
print(numbers)
Output:
[1, 2, 3]
Mistake 2: Using a mutable default argument
def add_item(item, basket=[]):
basket.append(item)
return basket
print(add_item("apple"))
print(add_item("banana"))
Output:
['apple']
['apple', 'banana']
Default argument values are created only once, when the function is defined — not on every call. Because [] is mutable, every call that relies on the default shares the exact same list object, so items pile up across unrelated calls. The fix is to default to None and create a fresh list inside the function:
def add_item(item, basket=None):
if basket is None:
basket = []
basket.append(item)
return basket
print(add_item("apple"))
print(add_item("banana"))
Output:
['apple']
['banana']
Mistake 3: Removing items while looping over the same list
numbers = [1, 2, 2, 3, 4]
for n in numbers:
if n % 2 == 0:
numbers.remove(n)
print(numbers)
Output:
[1, 2, 3]
A for loop tracks a numeric position into the list. Every time remove() deletes an item, everything after it shifts one slot to the left, but the loop’s position keeps advancing — so it silently skips the item that shifted into the spot it just checked. Here the second 2 is skipped entirely and survives. The safe fix is to build a new list instead of mutating while iterating:
numbers = [1, 2, 2, 3, 4]
numbers = [n for n in numbers if n % 2 != 0]
print(numbers)
Output:
[1, 3]
Best Practices
- Use
sorted(my_list)when you need a new sorted list and want to keep the original order intact; usemy_list.sort()only when you’re fine losing the original order. - Prefer
extend()over repeatedappend()calls (or over+=in a loop) when adding many items — it’s clearer and avoids accidentally nesting a whole list inside another withappend(). - Never use a mutable object (list, dict, set) as a default argument value — default to
Noneand create the mutable object inside the function body. - When you need to remove items based on a condition, build a new filtered list with a list comprehension rather than mutating the list you’re iterating over.
- Use
copy()or slicing (my_list[:]) for a quick shallow copy, but reach forcopy.deepcopy()when the list contains nested mutable objects you also need to isolate. - Check membership with
x in my_listbefore callingremove()orindex()if you’re not surexis present — both raise exceptions on a missing value. - Favor
key=functions (often alambda) over a manual loop when sorting by a computed value — it’s faster and more idiomatic than writing your own comparison logic.
Practice Exercises
- Exercise 1: Start with
inventory = ["sword", "shield", "potion"]. Add"bow"to the end, insert"helmet"at index 1, then remove"shield". Print the final list. - Exercise 2: Given
scores = [55, 90, 78, 90, 42, 90], use list methods to print how many times90appears and the index of its first occurrence. - Exercise 3: Write a function
unique_sorted(values)that takes a list of numbers, removes duplicates, sorts the result in descending order, and returns the new list — without modifying the original list passed in. (Hint: build a new list rather than mutatingvaluesdirectly.)
Summary
- List methods split into mutating methods (
append,extend,insert,remove,pop,clear,sort,reverse), which change the list in place and mostly returnNone, and query methods (index,count,copy), which don’t mutate the original. - Assigning a list to another variable (
alias = original) copies the reference, not the data — usecopy()or a slice for an independent shallow copy. sort()sorts in place and returnsNone;sorted()returns a new sorted list and leaves the original alone.remove()andindex()search by value and raiseValueErrorif the value isn’t found;pop()works by position and raisesIndexErrorif out of range.- Never mutate a list while iterating over it directly — iterate over a copy, or build a new list with a comprehension instead.
- Never use a mutable default argument — default to
Noneand create the list inside the function.
