Python Time Complexity for Interviews

Updated · By

In CPython, list indexing and append are O(1), with append amortized, and inserting or popping at the front is O(n). dict and set lookups are O(1) on average and O(n) in the worst case. deque is about O(1) at both ends, heappop is logarithmic, and building a str by repeated concatenation is quadratic.

Where these figures come from

Python now publishes this table itself. The figures below are from Time complexity of operations on built-in types in the official documentation, which replaced the old wiki page, plus the deque and heapq pages for the two types it does not cover. All were read on November 5, 2026, in the Python 3.15.2 documentation.

The table describes CPython and says other implementations may differ. n is the number of elements in the container and k is an index or a repeat count. It also says the costs assume exact built-in types, so a subclass may behave differently.

list

From the list table. The two costs interviewers ask about are the front of the list and the slice.

OperationCostNote
l[k], l[k] = xO(1)
len(l)O(1)The length is stored, so nothing is counted.
l.append(x)O(1) amortizedOne append may be O(n) when the storage is resized.
l.pop()O(1)At the end of the list, nothing moves.
l.pop(k), del l[k]O(n - k)l.pop(0) moves everything after it. Pop is amortized.
l.insert(k, x)O(n - k)l.insert(0, x) moves everything. Amortized.
x in lO(n)
min(l), max(l)O(n)
l[i:j]O(j - i)A slice copies.
l.copy()O(n)
l.extend(t)O(len(t)) amortizedPlus the cost of iterating over t.
l1 + l2O(len(l1) + len(l2))Builds a new list.
l * kO(nk)
l.sort()O(n log n) worst caseOnly O(n) comparisons on input already sorted or reverse-sorted.

dict

From the dict table, which calls these average-case times. They assume a hash function robust enough to make collisions uncommon, well-distributed keys, and O(1) hashing and comparing. In the worst case, when every key hashes to the same value, each O(1) below becomes O(n). That is the sentence to have ready when you say "hash lookup."

OperationAverage costNote
key in dO(1)
d[key], d.get(key)O(1)
d[key] = valueO(1) amortizedOne insert may be O(n) when the table is resized.
del d[key], d.pop(key)O(1)
len(d)O(1)The length is stored.
d.update(t)O(len(t)) amortizedPlus the cost of iterating over t.
Iterating, d.copy()O(n)Scans the internal table, which removals do not shrink, so it can cost more than the current length suggests.

set

From the set table. The same caveats apply as for dict: these are average-case times, and in the worst case the O(1) operations take O(n).

OperationAverage costNote
x in sO(1)
s.add(x)O(1) amortized
s.discard(x), s.remove(x)O(1)
len(s)O(1)The length is stored.
s1 | s2O(len(s1) + len(s2))
s1 & s2O(min(len(s1), len(s2)))With a non-set argument t to intersection, O(len(t)).
s1 - s2O(len(s1))With a non-set argument t to difference, O(len(s) + len(t)).
s.copy()O(n)Scans the internal table, as with dict.

collections.deque

The table does not cover deque, so this is from the deque documentation, which gives its figures in prose. Use a deque for a queue: its whole purpose is to avoid what list.pop(0) does.

OperationCostWhat the docs say
append, appendleft, pop, popleftAbout O(1)"Approximately the same O(1) performance in either direction."
d[0], d[-1]O(1)"Indexed access is O(1) at both ends."
d[i] in the middleO(n)"Slows to O(n) in the middle." Use a list for fast random access.

heapq

This is also outside the table, and the heapq documentation states less than people assume. A heap in heapq is a plain list, so reading the smallest item is a list read at index 0.

OperationCostWhat the docs say
heap[0]O(1)The docs say to read the smallest item this way, and the table gives a list read at O(1).
heapify(x)Linear"Transform list x into a min-heap, in-place, in linear time."
Removing the smallest itemLogarithmicThe Theory section: "clearly logarithmic on the total number of items in the tree."
Heapsort, by popping every itemO(n log n)The Theory section: "you get an O(n log n) sort."

Since Python 3.14, heapq also has heapify_max, heappush_max and heappop_max. On older versions, push negated values to get a max-heap.

str

From the str table. Strings are immutable, so copying one returns the same object, but every concatenation builds a new one.

OperationCostNote
s[k]O(1)
len(s)O(1)The length is stored.
s[i:j]O(j - i)A slice copies.
s + tO(len(s) + len(t))Concatenating in a loop is quadratic in the total length.
s * kO(nk)
x in s, s.find(x), s.index(x)O(n)The forward search has a linear worst case.
s.rfind(x), s.rindex(x)O(n × len(x)) worst caseO(n) on typical input.
Iterating over sO(n)

For the fix, Python's programming FAQ recommends putting the pieces in a list and calling str.join at the end, and its sequence-types page says that gives a linear runtime cost.

Hidden costs interviewers catch

Each of these looks like a single cheap line, and each comes straight from a row above. They are also how a correct answer quietly becomes too slow.

from collections import deque

# 1. Membership in a list inside a loop: O(n) per check, O(n²) in all.
def first_seen_slow(items):
    seen, out = [], []
    for x in items:
        if x not in seen:          # list: O(n)
            seen.append(x)
            out.append(x)
    return out

def first_seen(items):
    seen, out = set(), []
    for x in items:
        if x not in seen:          # set: O(1) on average
            seen.add(x)
            out.append(x)
    return out

# 2. Using a list as a queue: pop(0) moves every remaining element.
def drain_slow(tasks):
    order = []
    while tasks:
        order.append(tasks.pop(0)) # O(n) per pop
    return order

def drain(tasks):
    queue, order = deque(tasks), []
    while queue:
        order.append(queue.popleft())   # about O(1) per pop
    return order

# 3. Building a string with +=: each step copies what came before.
def join_slow(words):
    out = ""
    for w in words:
        out += w                   # quadratic in the total length
    return out

def join(words):
    return "".join(words)          # linear in the total length

# 4. Slicing in recursion: each call copies the rest of the list.
def total_slow(nums):
    return nums[0] + total_slow(nums[1:]) if nums else 0   # O(n²) time

def total(nums, i=0):
    return nums[i] + total(nums, i + 1) if i < len(nums) else 0   # O(n)

Both versions of the fourth example still use O(n) stack space, and Python's recursion limit will stop either one on a long list, so a loop is better there. Mentioning that before you are asked is a good way to show you know where the cost lives.

How to say it out loud

What a strong candidate might say

I'll keep the seen values in a set, because membership in a list would be O(n) per check and O(n squared) overall. A set lookup is O(1) on average, so the whole pass is O(n) on average with O(n) extra space. In the worst case, with every key colliding, it degrades to O(n) per lookup, but I don't expect that with these keys. I'm also using a deque rather than a list for the queue, because popping from the front of a list moves every remaining element.

The habit to build is naming the condition along with the figure: average case for dict and set, amortized for append, and CPython if the interviewer pushes. You can practice saying it in a coding mock interview, and the Big O cheat sheet covers how to derive the rest.

What the docs do not state

These common operations have no figure in the pages above, so they have no row here. If one comes up, reason from the structure and say that is what you are doing.

  • heapq.heappush: the docs describe a binary heap but give no cost for a push.
  • heapq.nlargest and nsmallest: the docs compare them with sorting and min or max, but give no Big O.
  • deque.rotate and membership with in on a deque: no figure given.
  • str.join, str.split and str.replace: no row in the table. Only the linear total for join is stated, in prose.

Frequently asked questions

What is the time complexity of a Python dict?

Getting, setting, deleting and testing a key are O(1) on average, according to Python's documentation. Setting is amortized, because the table sometimes resizes. In the worst case, when every key hashes to the same value, each of those operations is O(n). Iterating over a dict and copying it are O(n).

Is list.pop(0) O(1) in Python?

No. Popping at index k of a list is O(n - k), so pop(0) moves every remaining element and is O(n). Popping from the end with pop() is O(1). If you need to remove from the front, use collections.deque, whose popleft is about O(1) according to the documentation.

Why is string concatenation in a loop slow in Python?

Strings are immutable, so each concatenation builds a new string and copies what came before. Python's documentation says the total cost is quadratic in the total string length. The recommended fix is to collect the pieces in a list and call str.join at the end, which it says gives a linear cost.

Try a mock interview free

3 interviews a month on the Free plan, up to 20 minutes each. No card needed.