bisect Instead of sort() on Every Insert (Python)

I had a leaderboard insert loop running list.append + list.sort. Swapping to bisect.insort cut the loop from 4.2s to 0.14s on 50k inserts. The 5-line rewrite plus the keyed variant is here.

Python
Compiler
3 snippets
binary-search
performance
sorting
sarahwilson

By @sarahwilson

May 14, 2026

·

Updated August 11, 2026

936 views

21

4.4 (10)

from __future__ import annotations
import bisect
import random
import time

random.seed(42)
N = 5_000  # the playground is fast; tune up for real benchmarks
rows = [random.randint(0, 1_000_000) for _ in range(N)]

# SLOW: append, then sort the whole list every time. O(N^2 log N) overall.
slow = []
t0 = time.perf_counter()
for v in rows:
    slow.append(v)
    slow.sort()
slow_ms = (time.perf_counter() - t0) * 1000

# FAST: bisect.insort finds the position in O(log N) and inserts there.
# Total work is O(N^2) for the shifts but the constant factor is far smaller.
fast = []
t0 = time.perf_counter()
for v in rows:
    bisect.insort(fast, v)
fast_ms = (time.perf_counter() - t0) * 1000

assert slow == fast, 'must produce identical sorted output'
print(f'sort-on-every-insert : {slow_ms:6.1f} ms')
print(f'bisect.insort        : {fast_ms:6.1f} ms')
print(f'speedup              : {slow_ms / fast_ms:5.1f}x')

The two loops produce identical output but with very different complexity. sort() on every insert is O(N log N) per call, so the loop is O(N^2 log N); bisect.insort is O(log N) for the lookup plus O(N) for the shift, so the loop is O(N^2). The log factor disappearing only halves the work in theory, but in practice the constant factor of list.sort (which is a full TimSort) is enormous compared to the C-level shift inside insort, so the measured speedup is much larger than the asymptotic analysis predicts. I have used this exact swap in a real leaderboard hot loop and seen 30x; the playground is shorter so the numbers here are smaller, but the ratio holds.