Removing items from a Python list offers four tools, and they are easy to mix up: remove takes a value, pop takes a position and hands the element back, del takes a position and hands back nothing, and clear empties the whole thing. Choose by what you know, not by habit.
What does remove do?
remove searches for a value and deletes the first element that matches. It removes exactly one item, even when the value appears several times, and it raises ValueError if the value is not in the list at all.
items = ["a", "b", "c", "b"]
items.remove("b")
print(items)
try:
items.remove("z")
except ValueError as exc:
print("ValueError:", exc)
Both halves of that behaviour matter. Because only the first match goes, removing every duplicate takes a loop rather than a single call. And because a missing value raises instead of doing nothing, you need to check membership first when the value might legitimately be absent.
def discard_first(items, value):
if value in items:
items.remove(value)
return True
return False
bag = ["x", "y"]
print(discard_first(bag, "x"), bag)
print(discard_first(bag, "x"), bag)
Watch out for one subtlety: remove compares with ==, not with identity. For plain values like strings and numbers that is exactly what you want, but for objects with a custom __eq__ it can match something you did not expect.
What does pop do?
pop works by index and returns the element it removed. With no argument it takes the last item, which makes it the natural way to treat a list as a stack. With an index it takes that position, and an out-of-range index raises IndexError: pop index out of range.
numbers = [10, 20, 30]
print("pop(0) returned:", numbers.pop(0))
print("list is now:", numbers)
print("pop() returned:", numbers.pop())
print("list is now:", numbers)
try:
numbers.pop(9)
except IndexError as exc:
print("IndexError:", exc)
Because pop returns the element, it is the method you want when the removed value still matters. Popping until the list is empty is a standard drain loop.
stack = [1, 2, 3]
while stack:
print("popped:", stack.pop())
print("empty:", stack)
Popping from the front works but is slow, because every remaining element has to shift along. When you genuinely need a queue, collections.deque gives you popleft, which does the same job without the shifting.
from collections import deque
jobs = deque(["email", "report", "backup"])
print("popleft:", jobs.popleft())
print("remaining:", list(jobs))
What does del do?
del removes by position and returns nothing. It works on a single index and on a slice, and it is the right tool when you know exactly where the item is and do not need its value.
numbers = [1, 2, 3, 4, 5]
del numbers[1]
print(numbers)
del numbers[1:3]
print(numbers)
One advantage of del over pop is that it works on things that are not lists: a dictionary key, an attribute, a slice of a tuple. In a list context the difference is mostly stylistic — del items[i] says "discard this slot", items.pop(i) says "hand me what was in this slot".
How do you empty a list?
clear() removes every element and keeps the same list object. The alternatives are lst[:] = [], which also empties in place, and lst = [], which does something subtly different: it creates a brand-new list and rebinds the name, leaving any other name pointing at the old one.
first = [1, 2, 3]
second = first
first.clear()
print("first:", first, "second:", second)
third = [1, 2, 3]
fourth = third
third = []
print("third:", third, "fourth:", fourth)
The first pair shows why clear is usually the right choice: both names point at the same list, and both see it empty. The second pair shows the trap: third is a new empty list, but fourth still holds the old contents. That difference is the whole subject of the copying lesson that follows this one.
There is a third option that is almost always a mistake. Calling lst.remove(...) in a loop until it raises ValueError empties the list, but it relies on an exception as a loop condition and it stops at the first failure. clear() states the intent in one word.
Why does removing items while looping skip them?
Because the loop walks the list by index while the removal shifts the later elements one position to the left. The index you are about to visit now points at a different element than it did a moment ago, so exactly one survivor gets skipped each time you remove something.
numbers = [1, 2, 3, 4, 5, 6, 7, 8]
for n in numbers:
if n < 5:
numbers.remove(n)
print("unexpected result:", numbers)
The expected answer is [5, 6, 7, 8], and the actual answer keeps 2 and 4. Fix it by iterating over a copy, which leaves the original positions alone while you remove from the real list.
numbers = [1, 2, 3, 4, 5, 6, 7, 8]
for n in numbers[:]:
if n < 5:
numbers.remove(n)
print("correct result:", numbers)
The other fix is to build a new list instead of mutating the old one, which is usually the clearer of the two because it states the intent plainly: keep the items that pass the test.
items = ["a", "b", "c", "d"]
kept = [item for item in items if item not in ("b", "d")]
print(kept)
print("original untouched:", items)
How do you remove duplicates and keep the original order?
Removing duplicates by value is awkward with remove, because it only ever deletes one match at a time. The neat solution is to rebuild the list with a structure that refuses duplicates, and in Python 3.7 and later a dictionary does that while remembering insertion order.
raw = ["b", "a", "b", "c", "a", "d"]
unique = list(dict.fromkeys(raw))
print(unique)
print("order preserved:", unique == ["b", "a", "c", "d"])
dict.fromkeys builds a dictionary whose keys are the items you gave it and whose values are all None. A dictionary keeps one entry per key, so duplicates collapse automatically, and it keeps them in the order they first appeared. Turning the keys back into a list gives you the deduplicated sequence in the original order.
A set does the deduplication too, but it does not promise an order, so if the order of first appearance matters to you, the dictionary version is the one to reach for.
Removal patterns
| You want to… | Use | Returns | Error if absent |
|---|---|---|---|
| Drop one known value | remove(value) | None | ValueError |
| Take an item out by position | pop(index) | the element | IndexError |
| Take the last item out | pop() | the element | IndexError |
| Discard a slot or a range | del lst[i] | None | IndexError |
| Empty the list in place | clear() | None | never |
Try it yourself: start with ["red", "green", "blue", "red", "yellow"]. Remove one "red" with remove and note which one went. Remove everything below a threshold with a list comprehension and confirm the original list is untouched. Then pop the last element, pop by index, use del on a slice, and finish with clear. Finally, reproduce the skipping bug on purpose with a list of ten numbers and comment the line that explains exactly which numbers survive and why.