How to Sort a List in Python Without Using the sort() Function

You can sort a list in Python without the sort function by writing the loop yourself. Insertion sort is the one I’d reach for first.

Walk the list and slide each value left until the one before it is smaller. It’s short and hard to get wrong:

for i in range(1, len(nums)):
    cur, j = nums[i], i - 1
    while j >= 0 and nums[j] > cur:
        nums[j + 1] = nums[j]; j -= 1
    nums[j + 1] = cur

No example leans on sort() or sorted(), except as a yardstick at the end. Outputs are real runs on Python 3.12.5 in Command Prompt.

You’ll get the three classic interview answers, a faster merge sort, a heapq shortcut and a version with key and reverse.

Sort a list without sort() using insertion sort

Insertion sort grows a sorted part on the left. Each new value moves left past every bigger value, then drops into the gap:

def insertion_sort(items):
    result = list(items)              # a copy, like sorted()
    for i in range(1, len(result)):
        current = result[i]
        j = i - 1
        while j >= 0 and result[j] > current:
            result[j + 1] = result[j]     # shift bigger values one slot right
            j -= 1
        result[j + 1] = current
    return result

prices = [24.99, 9.50, 129.00, 4.75, 59.99, 9.50]
ordered = insertion_sort(prices)
print("Original:", [f"${p:.2f}" for p in prices])
print("Sorted:  ", [f"${p:.2f}" for p in ordered])

Output:

Original: ['$24.99', '$9.50', '$129.00', '$4.75', '$59.99', '$9.50']
Sorted:   ['$4.75', '$9.50', '$9.50', '$24.99', '$59.99', '$129.00']
Command Prompt output of a Python insertion sort ordering a list of USD prices without the sort() function
The six prices come back from lowest to highest, and the duplicate $9.50 stays in the list twice.

Insertion sort is fast on short or nearly sorted lists, which is why real libraries use it for small runs. The wider family is compared in my guide to sorting algorithms in Python.

Bubble sort a Python list and stop early

Bubble sort swaps out-of-order neighbors, so each pass bubbles the largest remaining value to the end. Most versions forget the swapped flag:

def bubble_sort(items):
    result = list(items)
    n = len(result)
    for pass_no in range(1, n):
        swapped = False
        for j in range(n - pass_no):
            if result[j] > result[j + 1]:
                result[j], result[j + 1] = result[j + 1], result[j]
                swapped = True
        if not swapped:                   # nothing moved: the list is already in order
            print(f"  stopped after pass {pass_no}")
            break
    return result

print(bubble_sort([72, 65, 88, 91, 54]))
print(bubble_sort([10, 20, 30, 50, 40]))   # almost sorted: finishes early

Output:

[54, 65, 72, 88, 91]
  stopped after pass 2
[10, 20, 30, 40, 50]
Command Prompt output of a Python bubble sort with an early exit flag stopping once a pass makes no swaps
Both lists end up in ascending order, and the nearly sorted one stops early after a pass with no swaps.

Without that flag, bubble sort always makes every pass, even on a list that’s already sorted. There’s a step-by-step trace in this Python program for bubble sort.

Selection sort: pick the smallest value each time

Selection sort scans the unsorted part, finds the smallest value, and swaps it to the front.:

def selection_sort(items):
    result = list(items)
    for i in range(len(result) - 1):
        smallest = i
        for j in range(i + 1, len(result)):
            if result[j] < result[smallest]:
                smallest = j
        result[i], result[smallest] = result[smallest], result[i]
    return result

temps = [68, 75, 59, 82, 75, 61]          # daily highs in Fahrenheit
print(selection_sort(temps))

Output:

[59, 61, 68, 75, 75, 82]

Selection sort makes at most n – 1 swaps, which matters when swapping is expensive. A longer walk-through lives in the selection sort program in Python.

Merge sort a list in O(n log n) without sorted()

The three loops above are O(n²): double the list and they take about four times as long. Merge sort splits the list in half, sorts each half, then merges the two sorted halves:

def merge_sort(items):
    if len(items) <= 1:
        return list(items)
    mid = len(items) // 2
    left, right = merge_sort(items[:mid]), merge_sort(items[mid:])
    merged, i, j = [], 0, 0
    while i < len(left) and j < len(right):
        if left[i] <= right[j]:   # <= keeps equal items in their original order
            merged.append(left[i]); i += 1
        else:
            merged.append(right[j]); j += 1
    return merged + left[i:] + right[j:]

zips = [94105, 10001, 60601, 33101, 2134, 73301, 98101]
print(merge_sort(zips))

Output:

[2134, 10001, 33101, 60601, 73301, 94105, 98101]

Each recursive call handles a smaller slice. If recursion feels slippery, how recursion works in Python is worth a quick read first.

Can you sort a list without sort() using heapq?

Yes. The heapq module isn’t a sort function, but a min-heap always hands back its smallest item. Pop everything and you’ve got a sorted list in O(n log n):

import heapq

def heap_sort(items):
    heap = list(items)
    heapq.heapify(heap)                   # smallest value moves to heap[0]
    return [heapq.heappop(heap) for _ in range(len(heap))]

scores = [88, 92, 79, 95, 85, 70]
print(heap_sort(scores))

Output:

[70, 79, 85, 88, 92, 95]

For just the top few values, heapq.nlargest() skips the full sort. Heaps also power the priority queue in Python, which is where they really earn their keep.

Sort a list of dictionaries by key and in reverse without sort()

Here, the insertion sort takes a key function and a reverse flag, the same two options sorted() gives you:

def insertion_sort(items, key=lambda x: x, reverse=False):
    result = list(items)
    for i in range(1, len(result)):
        current = result[i]
        j = i - 1
        while j >= 0 and (key(result[j]) < key(current) if reverse else key(result[j]) > key(current)):
            result[j + 1] = result[j]
            j -= 1
        result[j + 1] = current
    return result

orders = [
    {"customer": "Emily", "state": "TX", "total": 149.99, "date": "2026-10-03"},
    {"customer": "James", "state": "CA", "total": 89.50, "date": "2026-10-01"},
    {"customer": "Olivia", "state": "NY", "total": 149.99, "date": "2026-09-28"},
    {"customer": "Noah", "state": "FL", "total": 312.00, "date": "2026-10-05"},
]

print("By total, highest first:")
for o in insertion_sort(orders, key=lambda o: o["total"], reverse=True):
    print(f"  {o['customer']:<7} {o['state']}  ${o['total']:.2f}")

print(insertion_sort(["Ohio", "texas", "Alaska", "iowa"], key=str.lower))

Output:

By total, highest first:
  Noah    FL  $312.00
  Emily   TX  $149.99
  Olivia  NY  $149.99
  James   CA  $89.50
['Alaska', 'iowa', 'Ohio', 'texas']
Command Prompt output of a Python insertion sort with key and reverse ordering 2026 orders by total in descending order
Orders are listed by total from highest to lowest, with the tie kept in input order, and the state names sort case-insensitively.

Emily and Olivia both spent $149.99, and Emily stays ahead because she came first in the input. The strict comparison keeps it stable. For nested lists, see how to sort a list of lists in Python.

How fast is a hand-written sort compared with sorted()?

I timed insertion sort, merge sort and the built-in on 2,000 random integers. The assert line checks that all three agree before any timing starts:

import random, timeit
from sorts import insertion_sort, merge_sort    # the two functions from above, saved as sorts.py

random.seed(2026)
data = [random.randint(1, 100_000) for _ in range(2_000)]
assert insertion_sort(data) == merge_sort(data) == sorted(data)   # same answer every way

for name, fn in [("insertion_sort", insertion_sort), ("merge_sort", merge_sort), ("sorted()", sorted)]:
    ms = timeit.timeit(lambda: fn(data), number=5) / 5 * 1000
    print(f"{name:<15} {ms:9.2f} ms per run")

Output:

insertion_sort      73.70 ms per run
merge_sort           3.31 ms per run
sorted()             0.19 ms per run
Command Prompt output timing a Python insertion sort and merge sort against sorted() on 2,000 random integers
Insertion sort is the slowest of the three by a wide margin, merge sort lands in between, and sorted() is far faster than both.

Your exact milliseconds will differ, but the order won’t. The test list itself is built with a list comprehension, the same trick from initializing a list of size N in Python.

So write these to learn or to pass an interview. In production, the built-ins are written in C and will beat anything in pure Python.

Python sorting methods without sort() compared

MethodTimeStableBest for
Insertion sortO(n²)YesShort or nearly sorted lists
Bubble sortO(n²)YesLearning; early exit on sorted input
Selection sortO(n²)NoFewest swaps
Merge sortO(n log n)YesLarger lists, records with ties
heapq popsO(n log n)NoTop-N values, streams

Want to keep working with lists? Start with one of these:

Frequently asked questions

How do I sort a list in Python without using sort()?

Write a short insertion sort: shift each value left past every larger one. It works for numbers and strings.

How do I sort a list in descending order without sort()?

Flip the comparison so you shift while the earlier value is smaller, or add a reverse flag as shown above.

Which sorting algorithm is easiest to write in Python?

Insertion sort. It’s under ten lines and usually beats bubble sort.

Is heapq allowed if I can’t use sort() or sorted()?

Usually, yes, since heapify() and heappop() aren’t sort functions. Some teachers still want the loop.

Why is my hand-written sort so much slower than sorted()?

Simple sorts are O(n²) and run as Python bytecode. The built-in is an O(n log n) algorithm written in C.

Where does Python document how sorted() works?

The official Sorting Techniques HOWTO covers key functions, reverse order and sort stability.