Common collection mistakes in Python trip up every Python programmer at some point. The same bugs appear in beginner code, in production systems, and in Stack Overflow questions that have been asked thousands of times. The patterns are predictable, the causes are rooted in how Python's object model and collection internals work, and the fixes are straightforward once you understand the underlying mechanism. This article collects the most common mistakes across all four collection types so you can recognize them in your own code and fix them before they cause hard-to-trace bugs.
The mistakes covered here are not theoretical edge cases. They are the bugs that experienced Python developers learn to spot during code review because they have been burned by each one at least once. Understanding why they happen is more valuable than memorizing the fixes, because the same underlying causes, aliasing, mutability, and algorithmic complexity, explain entire categories of bugs beyond the specific examples shown here.
Aliasing instead of copying
The most persistent beginner mistake across all collection types is using assignment when a copy is needed. Writing new = old for a list, dictionary, or set does not create a copy. It creates a second name that points to the same object. Any modification through either name affects the single shared object.
This mistake is especially common when passing collections to functions. If a function receives a list and modifies it, the caller's list is modified too unless the caller passed a copy. The defensive pattern is to copy at the boundary: either the caller passes a copy, or the function makes a copy of its input before modifying it. The article on shallow vs deep copy in Python collections covers which copy method to use for each situation.
The nested version of this mistake is the shared inner list created by the multiplication operator. Writing [[]] * 5 creates five references to a single empty list, not five independent empty lists. Appending to one appends to all. The fix is a comprehension that creates a new inner list on each iteration.
Modifying while iterating
Removing items from a list while looping over it causes the loop to skip items. When an item is deleted, every subsequent item shifts one position left, but the loop's internal index still advances by one. The item that moved into the current position is never processed. The same issue occurs when adding items to a list during iteration, which can cause an infinite loop if the addition triggers further additions.
numbers = [1, 2, 3, 4, 5]
for n in numbers:
if n % 2 == 0:
numbers.remove(n)This code intends to remove even numbers but the result is [1, 3, 5]. It happens to work for this specific case, but the mechanism is unreliable. The loop skips 3 after removing 2 because 3 shifts into index 1 just as the loop advances from index 1 to index 2.
The safe alternatives are to iterate over a copy, to build a new collection with a comprehension, or to iterate backward so that removals only affect positions the loop has already passed. The comprehension approach is usually the clearest: it creates a new list containing only the items you want to keep, and it replaces the entire original list in one assignment.
Using the wrong type for lookups
Membership testing with the in operator looks identical across all collection types, which masks a dramatic performance difference. Checking membership in a list scans from the beginning, taking time proportional to the list's length. Checking membership in a set or a dictionary key takes constant time.
The mistake is not a syntax error but a performance trap. Code that works correctly during testing with small data can become unusably slow in production with real data volumes. A loop that checks membership against a list on every iteration will run in quadratic time, and the slowdown appears suddenly when the data crosses a threshold. The fix, replacing the list with a set, is a one-line change that reduces the complexity from quadratic to linear.
Shared defaults in function definitions
A subtle mistake that involves collections is using a mutable default argument in a function definition. Python evaluates default arguments once, when the function is defined, not each time the function is called. If the default is a mutable collection like a list or dictionary, every call that modifies that default modifies the same persistent object.
def add_item(item, target=[]):
target.append(item)
return targetThe first call with a single argument returns [item] as expected. The second call returns [item, item] because the same list accumulates across calls. The fix is to use None as the default and create the mutable collection inside the function body when the argument is None. This pattern is the standard approach for any function that needs a fresh mutable collection as a default.
Confusing dictionary keys with values
The in operator on a dictionary checks keys, not values. Writing value in my_dict tests whether value is a key, which often surprises beginners who expect it to search the values. To check values, write value in my_dict.values(), but be aware that this scans all values linearly and is not constant-time.
The reverse mistake is accessing dictionary methods as if they were attributes. The expressions my_dict.keys, my_dict.values, and my_dict.items without parentheses are method objects, not the view objects that the methods return when called. Forgetting the parentheses produces a confusing error message or, worse, passes silently if the method object is used in a context that does not immediately fail.
The article on choosing the right Python collection type provides a structured framework for making these decisions before you write the code, which prevents many of the mistakes covered here from occurring in the first place.
Rune AI
Key Insights
- Assignment does not copy; use slicing, .copy(), or deepcopy when you need an independent collection.
- Never modify a list or dictionary while iterating over it; iterate over a copy or build a new collection.
- Use sets for membership testing on large data; the in operator on a list is linear, not constant.
- Create nested collections with comprehensions, not multiplication, to avoid shared inner references.
- Check dictionary keys with in for constant time; checking values scans linearly.
Frequently Asked Questions
Why does modifying a list inside a for loop sometimes skip items?
Why does my nested list have the same values in every row?
Why is my dictionary lookup slow even though dictionaries are supposed to be fast?
Conclusion
Every Python programmer encounters these collection mistakes, usually in their first few months. Recognizing the patterns, understanding why they happen, and knowing the idiomatic fix for each one is what turns those frustrating debugging sessions into quick corrections.
More in this topic
Define and Call Python Functions
Learn the exact syntax for defining and calling Python functions with the def keyword, including naming rules, the function body, and how the call stack works.
Python Function Parameters and Arguments
Learn the difference between parameters and arguments, how to pass values by position and by keyword, and the rules for default parameter values.
Python Functions Explained
Learn what Python functions are, why they matter for organizing code, and how they form the foundation of non-trivial programs.