Lists ship with a built-in toolkit of methods for adding items, removing them, finding them, and emptying the whole thing out. This lesson covers the methods you'll reach for daily: append, extend, insert, remove, pop, index, count, clear, and reverse. Each one has a behavior quirk worth knowing (some mutate in place, some raise, some look cheap but aren't) and we'll work through each in an e-commerce setting: a shopping cart that fills up, loses unavailable items, and gets searched.
append(x) adds a single item to the end of the list. It mutates the list in place and returns None.
The cart now has three items, in the order they were added. append takes exactly one argument, and that argument becomes one new element. If you pass a list, the whole list becomes a single element:
The cart now has two elements: a string and a nested list. If you wanted to add two cables as separate items, append is the wrong tool. Reach for extend instead.
Cost: append is O(1) amortized. Python lists keep some spare capacity at the end, so most appends are constant-time. Every so often the list grows its underlying storage, which is O(n), but spread across many calls the average per-append cost stays constant.
A common bug worth flagging now. Because append returns None, code like this looks reasonable but breaks:
What's wrong with this code?
append mutates the list and returns None. The expression ["Wireless Mouse"].append("USB Cable") evaluates to None, which is then bound to cart. The list itself was modified, but no one kept a reference to it. The fix is to call append as a separate statement after the list exists.
Fix:
This "returns None" pattern repeats across most list methods. It applies to sort, reverse, insert, and others. Anything that mutates the list in place returns None, by design.
extend(iterable) adds every item from an iterable onto the end of the list, one at a time. It mutates in place and returns None.
Compare this with append(["USB Cable", "HDMI Cable"]), which would produce a nested list. extend unpacks the iterable: each element becomes its own list entry.
The argument can be any iterable, not just a list. Tuples, sets, strings, generators, even another list's slice all work:
The tuple gets unpacked into two strings. The string "AB" gets unpacked into individual characters because a string is iterable too. That last one is usually a bug if you didn't mean it. Be careful when extending with strings; if you wanted the string as a single element, use append.
You can also concatenate lists with +:
+ builds a new list and leaves the original alone. extend modifies the list in place. The difference shows up when something else holds a reference to your list:
After extend, the saved reference reflects the change because there's only one list. After the + reassignment, cart points to a new list, but saved_reference still points to the old one, which is now stale.
A quick reference for picking between them:
| Operation | Returns | Mutates Original? | Accepts |
|---|---|---|---|
cart.append(x) | None | Yes | One item |
cart.extend(it) | None | Yes | Any iterable |
cart + other | New list | No | Another list only |
cart += other | (rebinds) | Yes (same as extend) | Any iterable |
The += operator is interesting: on a list it behaves like extend, mutating in place. Mixing + and += mentally is one of the easier ways to write a bug, so be deliberate about which one you mean.
insert(i, x) puts x at position i, shifting every existing element from index i onward one position to the right.
USB Cable slots in at index 1, and the original HDMI Cable moves from index 1 to index 2. insert returns None, like the other mutating methods.
Index 0 inserts at the front:
An index equal to or larger than the list length just appends:
This is forgiving but easy to misread. If you mean "add to the end", use append; it's clearer and faster. Negative indices work too: insert(-1, x) puts the item before the last element.
Cost: insert(i, x) is O(n) because every element from index i to the end shifts one position to the right. insert(0, x) in particular has to move the whole list each call. If you need cheap left-inserts, reach for collections.deque, which supports O(1) appendleft.
A concrete demonstration of why insert(0, ...) hurts at scale. The pattern below is fine for a small cart, but if you're building a queue of thousands of orders this way, it's quadratic:
Same result, but the deque version stays O(1) per insert. For now, remember: front-insert lots of items, use deque.
remove(value) finds the first occurrence of value in the list and deletes it. It mutates in place, returns None, and raises ValueError if the value isn't there.
If the value appears more than once, only the first one goes:
One of the two USB Cable entries is gone; the other remains. To remove every occurrence, you'd loop or use a list comprehension.
Now the error case. If the value isn't in the list, remove raises:
You have two defensive options. Either check first with in, or wrap the call in try/except:
That works, but it walks the list twice: once for in, once for remove. For a small cart, fine. For a list of millions, watch out.
Cost: Both in and remove(value) are O(n): they scan from the front until they find the value. The "check then remove" pattern is two scans. If the value is usually present, try/except is cheaper because the success path scans once. If the value is usually absent, in first is fine.
For frequent membership checks, the right move is usually to use a set instead of a list. Sets do membership in O(1).
pop(i=-1) removes the item at index i and returns it. With no argument it removes the last item, which is its most common use.
pop is unusual among the mutating methods: it returns something useful. That makes it the right tool when you want both the removed item and the list-without-it. The classic use is implementing a stack:
Pass an index to pop from a specific position:
Negative indices count from the end, the same as elsewhere in Python. pop(-1) is the same as pop() with no argument.
If the index is out of range, pop raises IndexError:
Cost: pop() from the end is O(1). pop(0) and any non-tail pop(i) are O(n), because every element after index i shifts down by one. If you need O(1) popleft, use collections.deque.
So pop() and pop(-1) are cheap. pop(0) is the expensive twin of insert(0, ...), and the same advice applies: use deque if you're doing this in a loop.
index(value, start=0, stop=len) returns the position of the first occurrence of value. Like remove, it raises ValueError if the value isn't in the list.
The optional start and stop arguments restrict the search to a slice of the list (the slice is interpreted using the original list's indices, not the slice's):
Searching for "Mouse" starting from index 2 finds the one at index 2 itself, since start is inclusive. To skip past it:
Missing values raise:
Same defensive options as remove: check with in first, or wrap in try/except. The same O(n) caveat applies: index scans linearly. If you're doing this often on the same large list, a dict from value to index will turn the operation into O(1).
count(value) returns the number of times value appears in the list. It doesn't raise on missing values; it just returns 0.
A practical e-commerce use: counting quantities in a cart that allows duplicates instead of a (product, quantity) tuple.
That works, but notice: this loop calls count once per unique product, and each call scans the whole list. For three uniques in a six-item cart, that's three scans of six items. For a thousand uniques in a million-item cart, that's a thousand scans of a million items.
Cost: count(value) is O(n), and looping count over multiple values is O(n x k) where k is the number of distinct values. For counting many values at once, collections.Counter walks the list once and returns a dict of counts.
Counter is the right tool the moment you're counting more than one or two values.
clear() removes every element from the list, leaving it empty. It mutates in place and returns None.
There are three common ways to empty a list, and they're not all the same. This matters when other variables (or other parts of your program) hold a reference to the list:
clear mutates the underlying list. saved and cart_a are two names for the same list, so both see it empty.
The slice-deletion form does the same thing:
del cart_a[:] deletes every element in the list via a full-list slice. The underlying object is the same one saved points to, so saved sees the change.
The reassignment form is different:
cart_a = [] doesn't empty the existing list. It builds a fresh empty list and rebinds the name cart_a to it. The original list is unchanged, and saved still points to it. This is the source of a lot of confusion.
A reference for the three forms:
| Form | Mutates the existing list? | Other references see the change? |
|---|---|---|
cart.clear() | Yes | Yes |
del cart[:] | Yes | Yes |
cart = [] | No (creates a new list) | No |
If you only have one name pointing to the list, all three look the same. The moment something else holds a reference, the choice matters.
reverse() reverses the list in place. It mutates and returns None.
That's the whole behavior. It's the in-place sibling of reversed(list), which returns a new iterator without touching the original. reverse mutates, returns None, and is O(n).
Most of the methods on this page show up in the same cart's lifetime. The diagram below traces a typical sequence: the customer starts with an empty cart, adds items, removes one that turns out to be unavailable, and finally pops the last item before checkout.
The cart grows with append and extend, shrinks with remove (by value, when a product turns out to be unavailable), and the final pre-checkout step uses pop to peel off the last item and look at it. Each operation mutates the same list; no copies are made. Code that matches the diagram:
That one snippet uses five of the methods in this lesson, all on the same cart.
A consolidated table of everything covered in this lesson. Use it as a cheat sheet when you're deciding which method to reach for.
| Method | Returns | Mutates? | Raises | Complexity |
|---|---|---|---|---|
cart.append(x) | None | Yes | None | O(1) amortized |
cart.extend(iterable) | None | Yes | None | O(k) where k is iterable length |
cart.insert(i, x) | None | Yes | None | O(n), O(n) even for insert(0, ...) |
cart.remove(value) | None | Yes | ValueError if missing | O(n) |
cart.pop() | Removed item | Yes | IndexError if empty | O(1) at end |
cart.pop(i) | Removed item | Yes | IndexError if out of range | O(n) for non-tail |
cart.index(value) | Position | No | ValueError if missing | O(n) |
cart.count(value) | Count | No | None (returns 0) | O(n) |
cart.clear() | None | Yes | None | O(n) |
cart.reverse() | None | Yes | None | O(n) |
Two patterns to notice in the table. First, every method that mutates returns None. The one exception is pop, which returns the removed item, which is why it's the method you'll see used in expressions more than the others. Second, only index and count are non-mutating; everything else changes the list.
10 quizzes