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']

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]

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']

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

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
| Method | Time | Stable | Best for |
|---|---|---|---|
| Insertion sort | O(n²) | Yes | Short or nearly sorted lists |
| Bubble sort | O(n²) | Yes | Learning; early exit on sorted input |
| Selection sort | O(n²) | No | Fewest swaps |
| Merge sort | O(n log n) | Yes | Larger lists, records with ties |
heapq pops | O(n log n) | No | Top-N values, streams |
Want to keep working with lists? Start with one of these:
- Sort a list alphabetically in Python
- Find the closest value in a list using Python
- Work with a list of lists in Python
- Sort a dictionary in Python
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.

Bijay Kumar is a 13-time Microsoft MVP with more than 18 years in software development, and the founder of Python Guides and TSinfo Technologies. He started out building .NET and SharePoint solutions at HP, TCS and KPIT before moving into Python, machine learning and AI, and he also builds web apps with TypeScript and React. He writes the tutorials here himself, and every example is run before publishing so you see the real output. More about Bijay · Microsoft MVP profile · LinkedIn