Choosing the right Python collection for the job is worth more than a hundred micro-optimizations. A list membership test in a loop is O(n squared), while a set membership test is O(n).
The difference between 10 milliseconds and 10 seconds often comes down to one wrong collection choice.
Each Python collection has a different performance profile. Knowing which operation is fast on which type lets you pick the right one before you write the loop.
The big four: cost per operation
| Operation | list | tuple | set | dict | deque |
|---|---|---|---|---|---|
| Index access (x[i]) | O(1) | O(1) | --- | O(1) key | O(1) |
| Membership (x in c) | O(n) | O(n) | O(1) | O(1) key | O(n) |
| Append right | O(1) | --- | O(1) add | O(1) | O(1) |
| Pop left | O(n) | --- | --- | --- | O(1) |
| Insert middle | O(n) | --- | --- | --- | O(n) |
This table tells you everything: if your loop does membership tests, use a set or dict, and if it appends and pops from the left, use deque.
If it accesses by index, use a list.
Membership testing: set crushes list
The most common performance mistake is using a list for membership tests inside a loop.
ids = list(range(100_000))
target = 99_999
# List: scans all elements one by one.
found = target in idsThe list version compares target against each element until it finds a match. Worst case: 100,000 comparisons. For the same test with a set, the result is instant.
ids = set(range(100_000))
target = 99_999
# Set: one hash computation, one lookup.
found = target in idsThe set version computes target's hash and checks one bucket. One step regardless of size.
For tiny collections, lists can be faster. Under about 50 items, the hash computation overhead of sets outweighs the linear scan cost. Measure with your actual data size.
Dictionary lookups vs list scanning
When you need to find a value by key, a dict is the answer. A list of tuples that you scan linearly is wrong for anything beyond a handful of items.
# O(n): scan list of tuples
users_list = [("alice", 30), ("bob", 25), ("carol", 28)]
for name, age in users_list:
if name == "carol":
found = age
break
# O(1): dict lookup
users_dict = {"alice": 30, "bob": 25, "carol": 28}
found = users_dict["carol"]The dict version is a direct hash table lookup. The list version is a linear scan. With 10,000 users, the dict does one operation; the list does up to 10,000.
Deque for queue operations
Using list.pop(0) to remove the first element is O(n) because every remaining element shifts left. Use collections.deque for O(1) operations at both ends.
from collections import deque
queue = deque(["task1", "task2", "task3"])
queue.append("task4") # O(1) right side
first = queue.popleft() # O(1) left side
print(first) # "task1"A list would take O(n) for pop(0). A deque is implemented as a doubly-linked list of blocks, optimized for both-end operations.
Sorting: list.sort vs sorted
list.sort() sorts in place and is slightly faster than sorted() because it does not allocate a new list. Use sorted() when you need the original unsorted list preserved.
data = [5, 2, 9, 1, 5]
# In-place: modifies data, returns None.
data.sort()
# New list: original preserved, returns new list.
sorted_data = sorted(data)For large lists, the key parameter matters more than the sort method. Pass a key function to avoid expensive comparisons:
# Sort by a computed value, computed once per element.
words.sort(key=len)Counting and grouping
collections.Counter is a specialized dict optimized for counting. It is faster and cleaner than a manual loop with a regular dict.
from collections import Counter
words = ["apple", "banana", "apple", "cherry", "banana", "apple"]
counts = Counter(words)
print(counts.most_common(2))
# [("apple", 3), ("banana", 2)]Counter is implemented in Python but uses C-optimized dict operations internally. For grouping items by a key, use itertools.groupby after sorting, or collections.defaultdict for unsorted data.
from collections import defaultdict
groups = defaultdict(list)
for item in data:
groups[item.category].append(item)When the overhead of the right structure beats the wrong one
Sets and dicts have higher memory overhead than lists. Each set entry stores a hash value and a pointer, and the hash table itself is kept sparse to keep lookups fast. A set of 100,000 integers uses about 4 MB; a list uses about 800 KB for the same integers' pointers.
If your collection is small and you never do membership tests, the list's lower overhead wins. If you do membership tests, the set's O(1) lookup wins at any non-trivial size. The break-even point depends on your data, but for membership tests it is usually around 20 to 50 items.
The article on common Python performance bottlenecks covers how data structure choices interact with other patterns. For profiling which collection operations dominate your runtime, the article on cProfile walks through the measurement workflow.
Rune AI
Key Insights
- Sets and dicts give O(1) membership and lookup; lists give O(n).
- Use list for ordered indexed access, set for unique items and membership, dict for key-value lookups.
- collections.deque gives O(1) append/pop from both ends; list.pop(0) is O(n).
- Choose the collection based on the dominant operation: what does the loop body do most?
- For very small collections ( under 50 items), lists can be faster than sets due to lower overhead.
Frequently Asked Questions
When is a list faster than a set in Python?
Should I always use a dict for key-value lookups?
Conclusion
The right collection choice is often the single biggest performance decision in a Python program. A set for membership, a dict for key-value lookups, a deque for queue operations, and a list for ordered, indexed access. Know the O-notation for each operation and choose accordingly. The wrong collection in a hot loop can turn a 100ms operation into a 10-second one.
More in this topic
How to Test Python Code
Learn what testing means in Python, the core ideas behind automated tests, and the built-in and third-party tools that make testing practical.
Write Your First Python Unit Test
Write and run your first Python unit test step by step, using unittest to check a small function and understand the test output.
The Factory Pattern in Python
Learn how to use the factory pattern in Python to encapsulate object creation, making your code more flexible and easier to extend.