Python Time and Space Complexity

Describe how running time and memory grow with input size, recognise common complexity classes and pick data structures accordingly.

Lesson 81 of 107Advanced13 min read

Time complexity describes how the amount of work an algorithm does grows as its input grows; space complexity describes how its extra memory grows. Both are written in Big O notation: O(n) means work grows in proportion to the input size n, O(n²) means doubling n roughly quadruples the work.

Python
products = ["rice", "beans", "oats", "lentils"]
print("oats" in products)        # O(n): may check every item
stock = set(products)
print("oats" in stock)           # O(1) on average: a hash lookup

This lesson defines input size, walks through the common growth classes with Python examples, explains best, average and worst cases, and covers extra memory. The examples search and compare items in a food bank's pantry inventory. Complexity is the vocabulary used in the search and sorting lessons and in most coding interviews.

Input size

A complexity claim only makes sense once you say what n is. For a pantry, n might be the number of products, the number of donations, or the length of a product name, and different steps of a program may depend on different sizes:

Python
def has_product(products, name):   # n = len(products)
    for product in products:
        if product == name:
            return True
    return False

Big O keeps the fastest-growing term and drops constants: an algorithm doing 3n + 20 steps is O(n), because for large n the 3 and the 20 stop mattering to the shape of the growth. When there are two independent sizes, keep both: comparing every donation (d) against every product (p) is O(d × p), not O(n²).

Being explicit avoids confusing claims. "This is O(n)" about a function that loops over products and, inside, scans each product's name is really O(p × L), where L is name length; with short names that's effectively linear in p, but the assumption should be stated.

Growth classes

ClassNamePython examplen = 1,000 → 1,000,000
O(1)constantd[key], list.append(), len(x)same
O(log n)logarithmicbinary search, bisect~10 → ~20 steps
O(n)linearx in list, sum(), one loop×1,000
O(n log n)linearithmicsorted(), list.sort()×~2,000
O(n²)quadraticnested loops comparing all pairs×1,000,000
Python
def has_duplicate_slow(items):          # O(n²): compares every pair
    for i in range(len(items)):
        for j in range(i + 1, len(items)):
            if items[i] == items[j]:
                return True
    return False

def has_duplicate_fast(items):          # O(n) average: one pass with a set
    return len(set(items)) != len(items)

Big O isn't a stopwatch. It describes growth, not actual seconds: an O(n²) algorithm on 50 items can beat an O(n log n) one with heavy constant overhead. It tells you which approach will fall over as data grows, and that's when it matters most.

Worst and average cases

The same algorithm can do very different amounts of work depending on the input:

  • Best case: searching a list for the first item finds it immediately, 1 check.
  • Worst case: searching for an item that isn't there checks all n items.
  • Average case: for a random present item, about n/2 checks, which is still O(n).

Big O statements usually describe the worst case unless stated otherwise, because that's the guarantee you can rely on.

Python's sets and dictionaries are O(1) on average for lookups, inserts and deletes, thanks to hashing (see hash tables). That average assumes keys hash well; in pathological cases, many colliding keys, operations degrade towards O(n). In everyday code the average case holds, which is why "use a set for membership tests" is such reliable advice.

Some operations are amortised: list.append() occasionally has to copy the whole list to a bigger block, but averaged over many appends each one is O(1).

Auxiliary space

Space complexity usually counts the extra memory an algorithm needs beyond its input, called auxiliary space:

Python
def total_quantity(items):        # O(1) extra space: one running total
    total = 0
    for _, qty in items:
        total += qty
    return total

def sorted_names(items):          # O(n) extra space: builds a new list
    return sorted(name for name, _ in items)

Common sources of extra memory are copies (items[:], sorted()), new collections built in a loop or comprehension, sets used for fast lookup, and the call stack in recursive functions: recursion that goes n levels deep uses O(n) stack frames even if each frame is small.

There's often a trade-off: has_duplicate_fast above uses O(n) extra memory for its set to save time, while the slow version uses O(1) space. Choosing between them depends on which resource is scarce. Generators can reduce space dramatically by producing one item at a time instead of building a full list.

Independent practice and a worked check

Task: analyse an inventory scan, nested pair comparison and dictionary lookup with explicit assumptions and auxiliary memory.

Python
pantry = {"rice": 12, "beans": 0, "oats": 7, "lentils": 3}

def low_stock(pantry, limit):          # O(n) time, O(k) extra space for k results
    return [name for name, qty in pantry.items() if qty < limit]

def similar_pairs(names):              # O(n²) time: every pair
    return [(a, b) for i, a in enumerate(names) for b in names[i + 1:] if a[0] == b[0]]

def quantity(pantry, name):            # O(1) average time, O(1) space
    return pantry.get(name, 0)

print(low_stock(pantry, 5))                        # ['beans', 'lentils']
print(similar_pairs(["rice", "rye", "oats"]))      # [('rice', 'rye')]
print(quantity(pantry, "oats"))                    # 7

State n for each: the number of pantry products. Note that names[i + 1:] also copies part of the list on every iteration, adding memory use. For the worked check, the worst-case growth of a full list scan is O(n).

Reference and next practice

The Python wiki's TimeComplexity page lists costs for list, deque, set and dict operations. Continue with linear and binary search and sorting algorithms. Practise with the Python exercises and Python quiz.

Time and Space Complexity Summary

  • Input size: Complexity describes how work or memory grows with a defined input size.
  • Growth classes: Constant, logarithmic, linear and quadratic growth summarize common patterns.
  • Worst and average cases: Different inputs can require different amounts of work.
  • Auxiliary space: Count additional working storage separately from input and required output when useful.

Time and Space Complexity Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Time and Space Complexity in a small program

Python
products = ["rice", "beans", "oats", "lentils"]
checks = 0
for product in products:
    checks += 1
    if product == "oats":
        break
print(checks)
Output
3
Line-by-line explanation
  1. Lines 3 to 6A linear search counts how many items it inspects.
  2. Line 6break stops as soon as the target is found, so this search makes 3 checks, not 4. Searching for an absent product would check all 4, the worst case.
Example 2

Counting operations as n grows

  • Counting steps instead of timing makes the growth visible without measurement noise.
Python
def count_linear(n):
    steps = 0
    for _ in range(n):
        steps += 1
    return steps

def count_quadratic(n):
    steps = 0
    for i in range(n):
        for j in range(i + 1, n):
            steps += 1
    return steps

def count_binary(n):
    steps, low, high = 0, 0, n
    while low < high:                      # search for a value beyond the end
        mid = (low + high) // 2
        steps += 1
        low = mid + 1
    return steps

for n in [10, 100, 1000]:
    print(f"n={n:<5} linear {count_linear(n):<5} pairs {count_quadratic(n):<7} binary {count_binary(n)}")
Output
n=10    linear 10    pairs 45      binary 3
n=100   linear 100   pairs 4950    binary 6
n=1000  linear 1000  pairs 499500  binary 9
Line-by-line explanation
  1. Lines 7 to 12Comparing every pair takes n(n − 1)/2 steps: multiplying n by 10 multiplies the work by about 100.
  2. Lines 14 to 20Halving the search interval each step needs only about log₂ n steps.

Common Time and Space Complexity Mistakes

Membership tests on a list inside a loop

Don't
banned = load_banned_codes()                      # a list of 50,000 codes
valid = [c for c in donations if c not in banned]  # O(d × b)
Do
banned = set(load_banned_codes())                 # O(b) once
valid = [c for c in donations if c not in banned]  # O(d) on average

Each in on a list scans it; a set answers in constant average time.

Treating Big O as measured speed

Don't
"Algorithm A is O(n log n), so it will run faster than B, which is O(n²), on our 30-item list."
Do
"For 30 items either is fine; measure if it matters. A scales far better as the list grows."

Time and Space Complexity in Real Projects

Complexity in real systems

Performance problems in real applications are often accidental quadratic behaviour: a membership test on a list inside a loop, string concatenation in a loop, or a database query issued for every row of another query. They're invisible in testing with 50 records and painful in production with 50,000. Recognising the pattern and switching to a set, a dictionary, "".join() or a single query fixes it.

Python's documentation lists the time complexity of built-in operations. For real measurements, timeit and profilers such as cProfile show where time actually goes; Big O tells you what to expect as data grows.

Time and Space Complexity FAQ

What is Big O notation in simple terms?

It describes how an algorithm's work or memory grows as the input grows, ignoring constant factors. O(n) grows in step with the input; O(n²) grows with its square; O(1) stays the same regardless of size.

What is the time complexity of x in list versus x in set?

x in list is O(n) because Python may compare against every element. x in set and key in dict are O(1) on average because they use hashing to jump straight to the right place.

What is the time complexity of Python's sort?

sorted() and list.sort() use Timsort, which is O(n log n) in the worst case and O(n) when the data is already sorted or nearly so. It's also stable.

What is the difference between time and space complexity?

Time complexity measures how the number of operations grows; space complexity measures how extra memory grows. They often trade off: using a set or cache can save time at the cost of memory.

Is O(1) always faster than O(n)?

Not for every input size. O(1) means the cost doesn't grow, but that constant can be large; for tiny inputs an O(n) scan might be quicker. Big O predicts behaviour as inputs grow large, which is when the difference matters.

Time and Space Complexity Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Multiple choiceeasy

What is the worst case growth of a full list scan?

Multiple choicemedium

Which review concern is valid for time and space complexity?

Time and Space Complexity Quiz

2 questions to check what you learned.

  1. What is the worst case growth of a full list scan?

    • O(1)
    • O(n)
    • Python ignores the distinction
  2. Which statement correctly describes worst and average cases?

    • Different inputs can require different amounts of work.
    • Only the example output matters, not the rule
    • Every operation has the same behavior