Skip to main content
Complexity reference · CPython 3.10+

Big-O Cheatsheet for Python Builtins

TAs grade time complexity on every DATA 100, CSE 163, CS50P, and CMU 15-112 assignment that touches a 10000-row dataset. This page lists the average and worst-case Big-O for every operation on list, dict, set, frozenset, str, tuple, collections.deque, and heapq, plus 6 before-and-after snippets for the quadratic patterns students ship by accident.

7 builtin tables CPython average and worst case 6 before / after fixes
Reading the tables

5 complexity classes in 30 seconds

Big-O describes how runtime scales with input size n. The 5 classes below cover almost every Python homework problem. Memorize the shape of each, then read the operation tables like a lookup.

O(1)

Constant time. Runtime does not grow with input size. dict lookup, set membership, list append, and stack push all sit here. Aim for O(1) inside the inner loop of any sort or merge.

O(log n)

Logarithmic. Doubling the input adds one step. Binary search on a sorted list with bisect.bisect_left, and the heappush and heappop operations from heapq.

O(n)

Linear. Each element touched once. A single pass through a list, the in operator on a list, sum, max, and any list comprehension over the full sequence.

O(n log n)

Linearithmic. The cost of comparison-based sorting. list.sort, sorted, and heapify on an unsorted sequence land here. This is the cheapest sort Python ships.

O(n^2)

Quadratic. A loop inside a loop. list.pop(0) inside a for loop, repeated string concatenation with +=, and naive nested-loop search all degrade to this on a 10000-row dataset.

list

17 list operations with complexity

Python lists are dynamic arrays. Append and index by position are O(1). Anything that touches the front of the list, or scans for a value, costs O(n).

Operation Average Worst Note
append(x) O(1) amortized O(n) Amortized constant. Occasional resize copies the backing array.
pop() O(1) O(1) Pop from the end. No shift.
pop(0) O(n) O(n) Pop from the front. Shifts every remaining element left.
insert(0, x) O(n) O(n) Insert at the front. Shifts every existing element right.
insert(i, x) O(n) O(n) Shifts the tail. Equivalent cost to pop(i).
a[i] (index) O(1) O(1) Random access by integer position.
a[i] = x O(1) O(1) In-place assignment, no resize.
x in a O(n) O(n) Linear scan. For repeated lookup, convert to set first.
a[i:j] (slice) O(k) O(k) k is the slice length. Always a copy.
len(a) O(1) O(1) Stored as a field on the list object.
sort() O(n log n) O(n log n) Timsort. Stable. Mutates in place.
sorted(a) O(n log n) O(n log n) Timsort. Returns a new list.
reverse() O(n) O(n) In-place reversal. Touches every element.
count(x) O(n) O(n) Linear scan over the entire list.
min(a) / max(a) O(n) O(n) Single pass through the list.
a + b O(n + m) O(n + m) Concatenation copies both lists.
a * k O(n * k) O(n * k) Repetition. Copies the elements k times.
dict

10 dict operations with complexity

Python dicts are open-address hash tables. Average lookup, insert, delete, and membership are O(1). Worst case degrades to O(n) under hash collisions, which CPython mitigates with randomized hash seeds since 3.3.

Operation Average Worst Note
d[k] (get) O(1) O(n) Worst case requires hash-collision chain traversal.
d[k] = v (set) O(1) O(n) Amortized constant. Triggers resize when load factor passes 2/3.
del d[k] O(1) O(n) Marks the slot as deleted.
k in d O(1) O(n) Hash lookup. Worst-case linear under adversarial hashing.
d.get(k, default) O(1) O(n) Same complexity as d[k], no KeyError.
d.keys() / values() / items() O(1) O(1) Returns a live view. Iteration over the view is O(n).
iterate d O(n) O(n) Insertion-ordered since Python 3.7.
d.copy() O(n) O(n) Shallow copy of every key and value reference.
{**a, **b} merge O(n + m) O(n + m) Builds a new dict from both sources.
len(d) O(1) O(1) Tracked as a field.
set and frozenset

10 set operations with complexity

Sets share the dict hash-table backing minus the values. Membership and add are O(1) on average. Convert a list to a set the moment you need more than 3 membership tests against the same data.

Operation Average Worst Note
s.add(x) O(1) O(n) Hash insert. Same backing structure as dict.
s.remove(x) O(1) O(n) Raises KeyError when missing. Use discard for silent removal.
s.discard(x) O(1) O(n) Silent removal. Useful when membership is uncertain.
x in s O(1) O(n) The reason to convert a list to a set before repeated membership tests.
a | b (union) O(n + m) O(n + m) Builds a new set with every element from both.
a & b (intersection) O(min(n, m)) O(n * m) Iterates the smaller set against the larger.
a - b (difference) O(n) O(n) Walks a, checks each item against b.
a ^ b (symmetric) O(n + m) O(n + m) Items in either set but not both.
iterate s O(n) O(n) Order is undefined for sets (frozenset included).
len(s) O(1) O(1) Tracked as a field.
str

11 string operations with complexity

Strings in CPython are immutable Unicode. Every concatenation builds a new string. The quadratic trap appears when a student loops += over a long sequence; the linear fix is str.join on a list.

Operation Average Worst Note
a + b O(n + m) O(n + m) Strings are immutable. + builds a new string and copies both inputs.
s += x in a loop O(n^2) total O(n^2) total Quadratic over k concatenations. Use str.join on a list instead.
"".join(parts) O(n) O(n) Single allocation. The Pythonic concat pattern.
s.find(sub) O(n * k) O(n * k) k is the substring length. CPython uses a tuned two-way search.
s.count(sub) O(n * k) O(n * k) Counts non-overlapping occurrences across the whole string.
s.startswith / endswith O(k) O(k) k is the prefix or suffix length. Cheaper than full find.
s[i:j] (slice) O(k) O(k) Copies k characters into a new string.
s.split(sep) O(n) O(n) Single pass plus allocation for the result list.
s.replace(a, b) O(n) O(n) Single pass when both args are constants.
s.lower() / upper() O(n) O(n) New string. Unicode case folding under the hood.
len(s) O(1) O(1) Character count is stored on the object.
tuple

6 tuple operations with complexity

Tuples share the list array layout but are immutable, which makes them hashable. Use tuples as dict keys and set members when the data is fixed.

Operation Average Worst Note
t[i] O(1) O(1) Same backing layout as list, immutable.
x in t O(n) O(n) Linear scan. Same cost as a list scan.
t[i:j] O(k) O(k) Slice copy. Cheaper for small tuples than for large.
len(t) O(1) O(1) Stored as a field.
hash(t) O(n) O(n) Combines hashes of every element. Cached after first call.
t1 + t2 O(n + m) O(n + m) Allocates a new tuple, copies both inputs.
collections.deque

8 deque operations with complexity

A deque is a doubly linked list of fixed-size blocks. appendleft and popleft both run in O(1), which is the reason to pick deque over list for any queue, sliding window, or BFS frontier on a 6.006 or DATA 100 assignment.

Operation Average Worst Note
appendleft(x) O(1) O(1) Front insert. The reason to choose deque over list for queues.
append(x) O(1) O(1) Back insert, mirroring list.append.
popleft() O(1) O(1) Front removal. Replaces the O(n) list.pop(0).
pop() O(1) O(1) Back removal, mirroring list.pop.
d[i] (random index) O(n) O(n) Linked-block layout. Pay this cost only at the ends.
rotate(n) O(k) O(k) k is abs(n). Useful for circular buffers.
extend(iter) O(k) O(k) Appends k items at the back.
len(d) O(1) O(1) Tracked as a field.
heapq

7 heapq operations with complexity

heapq turns a list into a min-heap. heappush and heappop run in O(log n). heapify runs in O(n), which beats n separate pushes when you start from a flat list. Dijkstra, A-star, and the top-k pattern all live here.

Operation Average Worst Note
heappush(h, x) O(log n) O(log n) Bubbles up to maintain the heap invariant.
heappop(h) O(log n) O(log n) Returns the smallest item. Bubbles down to refill.
heapify(a) O(n) O(n) Bottom-up build is linear. Beats n separate pushes.
h[0] (peek) O(1) O(1) Smallest element sits at index 0.
heapreplace(h, x) O(log n) O(log n) Pop then push in one combined operation.
nlargest(k, a) O(n log k) O(n log k) Maintains a k-sized heap as it scans.
nsmallest(k, a) O(n log k) O(n log k) Mirror of nlargest.
Common blunders

6 quadratic patterns and their linear fix

These 6 anti-patterns account for most of the time-limit-exceeded marks on the autograded parts of CS50P, CSE 163, DATA 100, and CMU 15-112. Each block shows the slow version, the fix, and the line that changes.

Linear membership on a 10k list

Slow
                      
                        # x in big_list is O(n) per call
big_list = list(range(100000))
hits = 0
for item in [50, 1000, 99999]:
    if item in big_list:
        hits += 1
print(hits)               # 3, but the inner test scanned 100000 items each time
                      
                    
Fix
                      
                        # Convert once; membership becomes O(1)
big_list = list(range(100000))
lookup = set(big_list)
hits = sum(1 for item in [50, 1000, 99999] if item in lookup)
print(hits)               # 3, three constant-time lookups
                      
                    

list.pop(0) inside a for loop

Slow
                      
                        # Each pop(0) shifts every remaining element: O(n^2) total
data = list(range(10000))
collected = []
while data:
    collected.append(data.pop(0))   # O(n) each iteration
                      
                    
Fix
                      
                        # collections.deque has O(1) popleft
from collections import deque
data = deque(range(10000))
collected = []
while data:
    collected.append(data.popleft())  # O(1) each iteration
                      
                    

Repeated string concatenation

Slow
                      
                        # += on str inside a loop is O(n^2) over total length
parts = ['line ' + str(i) for i in range(5000)]
result = ''
for p in parts:
    result += p           # allocates a new string every iteration
                      
                    
Fix
                      
                        # str.join allocates once
parts = ['line ' + str(i) for i in range(5000)]
result = ''.join(parts)   # O(total length), single allocation
                      
                    

Counting with .count() in a loop

Slow
                      
                        # Each .count() is O(n). Calling it n times yields O(n^2).
words = 'the cat sat on the mat the dog barked'.split() * 1000
freq = {w: words.count(w) for w in set(words)}
print(freq['the'])
                      
                    
Fix
                      
                        # Counter scans the list once in O(n)
from collections import Counter
words = 'the cat sat on the mat the dog barked'.split() * 1000
freq = Counter(words)
print(freq['the'])
                      
                    

Sorting to find the smallest k items

Slow
                      
                        # Full sort is O(n log n) when you only want 5 items
nums = list(range(1000000))[::-1]
smallest = sorted(nums)[:5]
print(smallest)           # [0, 1, 2, 3, 4]
                      
                    
Fix
                      
                        # heapq.nsmallest is O(n log k); for k << n this wins
import heapq
nums = list(range(1000000))[::-1]
smallest = heapq.nsmallest(5, nums)
print(smallest)           # [0, 1, 2, 3, 4]
                      
                    

Dict key lookup with try / except as control flow

Slow
                      
                        # Exception handling is expensive when triggered often
counts = {}
for ch in 'abracadabra' * 100:
    try:
        counts[ch] += 1
    except KeyError:
        counts[ch] = 1
                      
                    
Fix
                      
                        # defaultdict avoids the exception path
from collections import defaultdict
counts = defaultdict(int)
for ch in 'abracadabra' * 100:
    counts[ch] += 1
                      
                    

Need a faster Python solution?

Send the brief, get a quote in 15 minutes, and receive Python code profiled against your target complexity. Pay 50% to start, 50% after the autograder passes.