Common Errors When Working with Python Lists and Fixes
A practical guide to the most frequent Python list errors, including IndexError, mutation pitfalls, and confusion between methods like append vs extend, with clear fixes and code examples.
Advertisement
If you've spent any time coding in Python, you've probably run into a few head-scratchers with lists. They seem simple enough, but they can trip you up in ways that make you question your sanity. I've been there, and I'm sure many readers of PythonSkillset have too. Let's walk through the most common list errors and how to fix them without the fluff.
IndexError: list index out of range
This is the classic. You're looping through a list, or trying to access an element, and Python throws this error. It happens when you try to grab an index that doesn't exist.
my_list = [10, 20, 30]
print(my_list[3]) # IndexError
The fix is simple: always check the length of your list before accessing an index. Use len() to know the bounds. If you're looping, use for item in my_list instead of indexing manually. If you need the index, use enumerate().
for i, item in enumerate(my_list):
print(f"Index {i}: {item}")
Modifying a List While Iterating Over It
This one is sneaky. You're looping through a list and removing items, but the loop skips elements or throws an error. Here's what happens:
numbers = [1, 2, 3, 4, 5]
for num in numbers:
if num % 2 == 0:
numbers.remove(num)
print(numbers) # [1, 3, 5]? Actually [1, 3, 4, 5] - wait, no
The issue is that when you remove an item, the list shifts, and the loop skips the next element. The fix? Iterate over a copy of the list.
numbers = [1, 2, 3, 4, 5]
for num in numbers[:]: # slice copy
if num % 2 == 0:
numbers.remove(num)
print(numbers) # [1, 3, 5]
Or better yet, use a list comprehension for filtering. It's cleaner and faster.
numbers = [1, 2, 3, 4, 5]
numbers = [num for num in numbers if num % 2 != 0]
Confusing == with is When Comparing Lists
This one catches beginners and even experienced devs off guard. You have two lists with the same values, but is returns False while == returns True.
a = [1, 2, 3]
b = [1, 2, 3]
print(a == b) # True
print(a is b) # False
== checks if the contents are equal. is checks if they're the same object in memory. If you're comparing lists, use == unless you specifically need to check identity. This is especially important when working with functions that return lists or when copying data.
Forgetting That Lists Are Mutable
Lists can be changed in place. This sounds obvious, but it causes bugs when you pass a list to a function and modify it accidentally.
def add_item(lst, item):
lst.append(item)
return lst
my_list = [1, 2, 3]
new_list = add_item(my_list, 4)
print(my_list) # [1, 2, 3, 4] - original changed!
If you don't want to modify the original, make a copy first. Use list.copy() or slice it.
def add_item_safe(lst, item):
new_lst = lst.copy()
new_lst.append(item)
return new_lst
Using + to Concatenate Lists When You Mean append
This one is subtle. You want to add an element to a list, but you use + instead of append. The result? A TypeError or unexpected behavior.
my_list = [1, 2, 3]
my_list = my_list + 4 # TypeError: can only concatenate list (not "int") to list
The fix: use append() for a single element, or extend() for adding multiple elements from another list.
my_list.append(4) # [1, 2, 3, 4]
my_list.extend([5, 6]) # [1, 2, 3, 4, 5, 6]
Forgetting That Slicing Returns a New List
When you slice a list, you get a new list object. This seems obvious, but I've seen people modify a slice and expect the original to change.
original = [1, 2, 3, 4, 5]
slice_copy = original[1:3]
slice_copy[0] = 99
print(original) # [1, 2, 3, 4, 5] - unchanged
If you want to modify the original through slicing, assign back to the slice.
original[1:3] = [99, 100]
print(original) # [1, 99, 100, 4, 5]
Using list.remove() in a Loop
list.remove() removes the first occurrence of a value. If you use it in a loop to remove all occurrences, you'll only remove the first one each time, and the loop might skip elements.
items = [1, 2, 3, 2, 4, 2]
for item in items:
if item == 2:
items.remove(item)
print(items) # [1, 3, 4, 2] - not all 2s removed
The fix: use a list comprehension or iterate over a copy.
items = [1, 2, 3, 2, 4, 2]
items = [item for item in items if item != 2]
print(items) # [1, 3, 4]
Confusing append with extend
This is a common mix-up. append adds its argument as a single element, even if that argument is a list. extend adds each element from an iterable.
a = [1, 2, 3]
a.append([4, 5])
print(a) # [1, 2, 3, [4, 5]]
b = [1, 2, 3]
b.extend([4, 5])
print(b) # [1, 2, 3, 4, 5]
If you want to add multiple items as separate elements, use extend. If you want to add a list as a single element, use append.
Using list.sort() When You Meant sorted()
list.sort() sorts the list in place and returns None. sorted() returns a new sorted list. If you assign the result of sort() to a variable, you get None.
my_list = [3, 1, 2]
sorted_list = my_list.sort()
print(sorted_list) # None
print(my_list) # [1, 2, 3] - original changed
If you want to keep the original list unchanged, use sorted().
my_list = [3, 1, 2]
sorted_list = sorted(my_list)
print(sorted_list) # [1, 2, 3]
print(my_list) # [3, 1, 2] - original intact
Forgetting That list.copy() Creates a Shallow Copy
This one bites you when you have nested lists. A shallow copy means the outer list is new, but the inner lists are still references to the same objects.
original = [[1, 2], [3, 4]]
copy = original.copy()
copy[0][0] = 99
print(original) # [[99, 2], [3, 4]] - original changed!
If you need a deep copy where everything is independent, use copy.deepcopy().
import copy
original = [[1, 2], [3, 4]]
deep_copy = copy.deepcopy(original)
deep_copy[0][0] = 99
print(original) # [[1, 2], [3, 4]] - safe
Using list.index() Without Checking If the Item Exists
list.index() raises a ValueError if the item isn't found. This can crash your program if you're not careful.
fruits = ['apple', 'banana', 'cherry']
position = fruits.index('orange') # ValueError
Always check if the item is in the list first, or use a try-except block.
if 'orange' in fruits:
position = fruits.index('orange')
else:
position = -1 # or handle it differently
Or use a try-except for cleaner code.
try:
position = fruits.index('orange')
except ValueError:
position = -1
Assuming list.pop() Returns the Last Element
list.pop() without an argument removes and returns the last element. But if you pass an index, it removes and returns that specific element. This is fine, but people forget that pop() modifies the list.
my_list = [1, 2, 3]
last = my_list.pop()
print(last) # 3
print(my_list) # [1, 2]
If you just want to remove an element without getting it back, use del instead.
my_list = [1, 2, 3]
del my_list[1]
print(my_list) # [1, 3]
Using list.insert() with Wrong Index
list.insert(index, element) inserts before the given index. If you use a negative index, it counts from the end. This can be confusing.
my_list = [1, 2, 3]
my_list.insert(-1, 99) # inserts before the last element
print(my_list) # [1, 2, 99, 3]
If you want to append to the end, use append(). If you want to insert at the beginning, use insert(0, element). Just remember that negative indices count from the end.
Assuming list.pop() Returns the Last Element When It's Empty
list.pop() without arguments removes and returns the last element. But if the list is empty, it raises an IndexError.
empty_list = []
last = empty_list.pop() # IndexError: pop from empty list
Always check if the list is non-empty before popping, or use a default value with a try-except.
if empty_list:
last = empty_list.pop()
else:
last = None
Using list.count() Inefficiently
list.count() is O(n) because it has to scan the entire list. If you're calling it in a loop, you're making your code slow.
my_list = [1, 2, 3, 2, 4, 2]
for item in my_list:
if my_list.count(item) > 1:
print(f"{item} appears multiple times")
This is O(n²) because for each element, you scan the whole list again. Instead, use a dictionary or collections.Counter to count once.
from collections import Counter
counts = Counter(my_list)
for item, count in counts.items():
if count > 1:
print(f"{item} appears {count} times")
Using list.pop(0) for a Queue
list.pop(0) removes the first element, but it's O(n) because all other elements have to shift. If you're building a queue, use collections.deque instead.
from collections import deque
queue = deque([1, 2, 3])
first = queue.popleft() # O(1)
print(first) # 1
print(queue) # deque([2, 3])
For most cases, lists are fine. But if you're doing a lot of pops from the front, deque is your friend.
Assuming list.reverse() Returns a New List
list.reverse() reverses the list in place and returns None. If you try to assign it, you'll get None.
my_list = [1, 2, 3]
reversed_list = my_list.reverse()
print(reversed_list) # None
print(my_list) # [3, 2, 1]
If you want a new reversed list, use reversed() or slicing.
my_list = [1, 2, 3]
reversed_list = list(reversed(my_list))
# or
reversed_list = my_list[::-1]
Using list.insert() at the End
list.insert() is meant for inserting at a specific position. If you're always adding to the end, use append(). It's faster and clearer.
my_list = [1, 2, 3]
my_list.insert(len(my_list), 4) # works but slower
my_list.append(4) # better
Forgetting That list Is a Built-in Name
This is a classic mistake. You name a variable list, and then you can't use the list() constructor anymore.
list = [1, 2, 3]
another_list = list([4, 5, 6]) # TypeError: 'list' object is not callable
Never name your variables after built-in types. Use my_list, items, or something descriptive.
Using list as a Default Argument
This is a well-known Python gotcha. Default arguments are evaluated only once when the function is defined, not each time the function is called.
def add_item(item, my_list=[]):
my_list.append(item)
return my_list
print(add_item(1)) # [1]
print(add_item(2)) # [1, 2] - not [2]!
The fix: use None as the default and create a new list inside the function.
def add_item(item, my_list=None):
if my_list is None:
my_list = []
my_list.append(item)
return my_list
Forgetting That list Is Zero-Indexed
This is basic but still trips people up. The first element is at index 0, not 1. If you're coming from a language that uses 1-based indexing, this takes getting used to.
my_list = ['a', 'b', 'c']
print(my_list[1]) # 'b', not 'a'
Always remember: index 0 is the first element. If you need the last element, use -1.
Using list as a Variable Name
I've seen this in production code. Someone names a variable list, and then they can't use the list() constructor anymore.
list = [1, 2, 3]
another_list = list([4, 5, 6]) # TypeError: 'list' object is not callable
Just don't do it. Use my_list, items, data, or anything else. It's a small habit that saves headaches.
Not Understanding List Comprehensions Scope
List comprehensions have their own scope in Python 3. Variables defined inside a list comprehension don't leak to the outer scope. This is different from Python 2, and it can cause confusion.
x = 10
squares = [x**2 for x in range(5)]
print(x) # 10, not 4
This is actually a good thing. It prevents accidental variable overwriting. But if you're used to the old behavior, it can surprise you.
Using list Methods That Modify in Place Without Realizing
Methods like sort(), reverse(), append(), and extend() modify the list in place and return None. If you chain them or assign the result, you'll get None.
my_list = [3, 1, 2]
new_list = my_list.sort()
print(new_list) # None
Always remember: if a method modifies the list in place, it returns None. If you want a new list, use functions like sorted() or reversed().
Not Handling Empty Lists in Loops
An empty list is falsy in Python, but that doesn't mean your loop will handle it gracefully. If you try to access an element without checking, you'll get an IndexError.
my_list = []
first = my_list[0] # IndexError
Always check if the list is non-empty before accessing elements.
if my_list:
first = my_list[0]
else:
first = None
Using list Methods That Return None
Methods like sort(), reverse(), append(), and extend() return None. If you try to chain them or assign the result, you'll get unexpected behavior.
my_list = [3, 1, 2]
sorted_list = my_list.sort()
print(sorted_list) # None
The fix: don't assign the result. Just call the method and use the original list.
Final Thoughts
Lists are one of Python's most versatile tools, but they have their quirks. The key is to understand that many list methods modify the list in place and return None. Once you get that, half the errors disappear. The other half come from forgetting about indexing, copying, and iteration.
At PythonSkillset, we believe that mastering these small details makes you a better programmer. The next time you hit a list error, take a breath, check your assumptions, and remember these fixes. Happy coding.
Advertisement
Comments
Questions, corrections, and tips stay visible for everyone reading this page.
Join the discussion
No comments yet
Be the first to leave a note — it helps the next reader.