The time complexity of Python’s data structures is what decides whether a program finishes in a blink or in an hour. Some operations that look identical are very different in cost: x in my_list checks items one by one (O(n)), while x in my_set jumps straight to the answer (O(1) on average), and list.pop(0) shifts every remaining item (O(n)) while deque.popleft() does not (O(1)).
This page is a practical reference: the cost of the operations you use most, with short demonstrations of the surprising ones. If Big O is new, start with Big O Notation Explained With Python Examples.
In this guide
The short version
- List: fast at the end (
append,pop()), slow at the front (insert(0, x),pop(0)) and forin. - Dict and set:
O(1)on average for lookup, add and delete. - deque:
O(1)at both ends. Use it as a queue. - heapq:
O(log n)to push or pop the smallest.O(1)to peek at it. - “Average” for hash-based types means typical behaviour. A pathological worst case is
O(n).
Rules of thumb
- Anything that scans the whole collection (
inon a list,remove,index,count,min,max,sum) isO(n). - Anything that jumps straight to a position or a key (list index, dict lookup) is
O(1). - Anything that has to shift or copy items (insert or delete in the middle or at the front of a list, slicing) is
O(n). - Anything that sorts is
O(n log n).
list
A Python list is a dynamic array: a block of memory holding references. That makes reading by position instant, and changing the end cheap, but anything at the front or in the middle expensive, because everything after it has to move:
| Operation | Example | Cost |
|---|---|---|
| Read or write by index | items[i] | O(1) |
| Length | len(items) | O(1) |
| Add at the end | items.append(x) | O(1) amortized |
| Remove from the end | items.pop() | O(1) |
| Insert at the front or middle | items.insert(0, x) | O(n) |
| Remove from the front or middle | items.pop(0), del items[i], items.remove(x) | O(n) |
| Membership test | x in items | O(n) |
| Find position | items.index(x) | O(n) |
| Slice | items[a:b] | O(b – a) |
| Copy | items.copy() | O(n) |
| Extend with k items | items.extend(more) | O(k) |
| Sort | items.sort(), sorted(items) | O(n log n) |
| Minimum, maximum, sum | min(items), max(items), sum(items) | O(n) |
items = [10, 20, 30, 40]
items.append(50)
print(items.pop())
print(items.pop(0))
items.insert(0, 5)
print(items)
print(30 in items)
Output
50
10
[5, 20, 30, 40]
True
“Amortized O(1)” for append means that an occasional append has to grow the underlying array, which takes longer, but spread across many appends the average cost is constant. The front is a different story. To see how different, compare removing 20,000 items from the front of a list and of a deque. The exact times vary from computer to computer, so this only prints whether the list was more than ten times slower:
import time
from collections import deque
n = 20000
lst = list(range(n))
dq = deque(range(n))
start = time.perf_counter()
for _ in range(n):
lst.pop(0)
t_list = time.perf_counter() - start
start = time.perf_counter()
for _ in range(n):
dq.popleft()
t_deque = time.perf_counter() - start
print(len(lst), len(dq))
print(t_list > 10 * t_deque)
Output
0 0
True
On a typical computer the real ratio is far larger than 10, and it gets worse as the list grows.
dict and set
Dictionaries and sets are hash tables. Python turns a key into a number (its hash) and uses it to go almost directly to where the item lives. That is why lookups are fast regardless of size (see Hash Tables in Python: Dict and Set for DSA):
| Operation | dict | set | Cost (average) |
|---|---|---|---|
| Look up | d[k], d.get(k), k in d | x in s | O(1) |
| Add or update | d[k] = v | s.add(x) | O(1) |
| Delete | del d[k], d.pop(k) | s.remove(x), s.discard(x) | O(1) |
| Loop over everything | for k in d | for x in s | O(n) |
| Copy | d.copy() | s.copy() | O(n) |
| Combine | d.update(other) | a | b | O(size of the added part) |
| Intersection | a & b | O(min(len(a), len(b))) | |
| Difference | a – b | O(len(a)) |
d = {}
d["a"] = 1
print(d.get("a"), "a" in d)
del d["a"]
print(len(d))
Output
1 True
0
The worst case for hash tables is O(n), when many keys collide in the same slot. Python’s hashing makes that very unlikely in practice, which is why people quote O(1). The practical payoff of a set over a list for membership tests is large. Here we look up a value 300 times in a collection of 100,000 items, and print whether the list took more than 20 times as long:
import time
n = 100_000
as_list = list(range(n))
as_set = set(as_list)
targets = [n - 1] * 300
start = time.perf_counter()
for t in targets:
t in as_list
t_list = time.perf_counter() - start
start = time.perf_counter()
for t in targets:
t in as_set
t_set = time.perf_counter() - start
print(n - 1 in as_list, n - 1 in as_set)
print(t_list > 20 * t_set)
Output
True True
True
If you do repeated in checks on a big list, converting it to a set first is one of the easiest speed-ups there is. Set operations are shown in more detail in Python List vs Tuple vs Set:
a = {1, 2, 3, 4}
b = {3, 4, 5}
print(a | b, a & b, a - b, a ^ b)
print(a.issubset({1, 2, 3, 4, 5}))
Output
{1, 2, 3, 4, 5} {3, 4} {1, 2} {1, 2, 5}
True
deque
collections.deque (double-ended queue) is built for adding and removing at both ends. It is the right tool for queues, and for sliding-window problems:
| Operation | Example | Cost |
|---|---|---|
| Add at either end | d.append(x), d.appendleft(x) | O(1) |
| Remove from either end | d.pop(), d.popleft() | O(1) |
| Rotate | d.rotate(k) | O(k) |
| Read the middle by index | d[i] | O(n) |
| Membership test | x in d | O(n) |
from collections import deque
d = deque([1, 2, 3])
d.append(4)
d.appendleft(0)
print(d)
print(d.pop(), d.popleft())
d.rotate(1)
print(d)
Output
deque([0, 1, 2, 3, 4])
4 0
deque([3, 1, 2])
The trade-off is that deque is not good at reading the middle by index. See Queue and Deque in Python Explained.
heapq (heaps)
The heapq module turns a plain list into a priority queue that always gives you the smallest item first:
| Operation | Example | Cost |
|---|---|---|
| Push | heapq.heappush(h, x) | O(log n) |
| Pop the smallest | heapq.heappop(h) | O(log n) |
| Peek at the smallest | h[0] | O(1) |
| Turn a list into a heap | heapq.heapify(items) | O(n) |
| k smallest items | heapq.nsmallest(k, items) | O(n log k) |
import heapq
h = []
for x in [5, 1, 8, 3]:
heapq.heappush(h, x)
print(heapq.heappop(h), heapq.heappop(h))
nums = [9, 4, 7, 1]
heapq.heapify(nums)
print(nums[0])
print(heapq.nsmallest(2, [9, 4, 7, 1]))
Output
1 3
1
[1, 4]
More in Heap and heapq in Python: Priority Queues. For a sorted list, the bisect module finds a position in O(log n), but inserting still costs O(n) because of the shifting:
from bisect import bisect_left, insort
data = [1, 3, 5, 7]
print(bisect_left(data, 5))
insort(data, 4)
print(data)
Output
2
[1, 3, 4, 5, 7]
Strings and tuples
Strings and tuples are immutable, so “changing” one always builds a new object:
- Index:
O(1). Slice:O(k)for a slice ofkcharacters. Length:O(1). - Concatenation
a + b:O(len(a) + len(b)), because a new string is created. Doing this repeatedly in a loop can add up toO(n²). Collect the pieces in a list and use"".join(parts)instead, which isO(total length). x in sfor a substring: roughly linear in the length of the string for typical inputs.- Tuples: index
O(1),inO(n).
parts = ["a", "b", "c"]
print("".join(parts))
s = ""
for p in parts:
s += p
print(s)
Output
abc
abc
Which structure for which job?
| You need to… | Use | Why |
|---|---|---|
| Read by position, add and remove at the end | list | O(1) for all three |
| Check membership or remove duplicates | set | O(1) average lookup |
| Look a value up by a key | dict | O(1) average lookup |
| Use it as a queue (first in, first out) | deque | O(1) at both ends |
| Always get the smallest (or largest) item next | heapq | O(log n) push and pop |
| Keep a sorted list and search it | list + bisect | O(log n) search |
| Build a big string piece by piece | list of parts + join | Avoids O(n squared) copying |
Common mistakes
- Using a list as a queue.
pop(0)isO(n). Usedeque. - Repeated
inchecks on a list. Build a set once, and check against that. - Slicing inside a loop.
items = items[1:]copies everything on each pass. - Inserting at the front of a big list again and again.
- Building a string with
+=in a loop when the data is large. - Sorting inside a loop when you only need the minimum. Use
min(), or a heap.
Try it yourself
Work out each answer first, then open the solution.
1. You need to process tasks in the order they arrive, adding new ones at one end and taking from the other. Which structure, and why?
Show answer
A deque: append to add and popleft to take are both O(1). A list would make pop(0) cost O(n).
from collections import deque
q = deque()
for x in [1, 2, 3]:
q.append(x)
print(q.popleft(), q.popleft())
Output
1 22. You read one million ids and need to know how many are different. What is the cheapest approach?
Show answer
Add each id to a set and take its length. Each add is O(1) on average, so the whole job is O(n). Checking a list for each id would be O(n²).
seen = set()
for x in [3, 1, 3, 2, 1]:
seen.add(x)
print(sorted(seen))
Output
[1, 2, 3]3. What is the cost of items.insert(0, x) on a list of n items, and why?
Show answer
O(n). Every existing item has to move one position to the right to make room.
Try the examples in our free Python compiler.
Frequently asked questions
What is the time complexity of list append in Python?
O(1) amortized. An individual append is usually constant time, and occasionally the list has to grow its storage, but averaged over many appends the cost per append is constant.
What is the time complexity of a dictionary lookup?
O(1) on average. In the very unlikely worst case, with heavy hash collisions, it degrades to O(n).
Is x in list O(n)?
Yes. Python compares against the items one by one until it finds a match or reaches the end. Use a set if you do many membership tests.
Why is list.pop(0) slow?
Removing the first item shifts every remaining item one place left, which takes O(n) time. A collections.deque removes from the front in O(1).
What is the complexity of sorted() in Python?
O(n log n) in the worst case. Python’s sort (Timsort) is also stable, and it is fast on data that is already partly ordered.
Are these complexities guaranteed by the language?
They describe how the standard CPython implementation behaves, and they are widely relied on. The language itself defines the behaviour, and implementations document their costs, so treat them as dependable but implementation-specific.
Related reading
- Big O Notation Explained With Python Examples – what these letters mean.
- Hash Tables in Python: Dict and Set for DSA – how the O(1) lookup works.
- Queue and Deque in Python Explained – the right structure for queues.
- Heap and heapq in Python: Priority Queues – always get the smallest next.
Run this code in your browser
The free Upskly compiler runs Python with nothing to install. Paste the example, change it and see what happens.
Stuck on a traceback?
AI Assist works inside the Python notebook, so you can ask about an error or a concept without leaving the cell.
Test yourself
Timed questions on Time and Space Complexity, with an explanation for every answer.