Python Sorting Algorithms

Sort by the right key, rely on stability for ties, understand insertion and merge sort, and use Python's built-in sorting in real code.

Lesson 83 of 107Advanced13 min read

Sorting puts items in order. In everyday Python you call sorted(items) or items.sort() with a key that says what to order by, and Python's built-in Timsort does the rest in O(n log n) time:

Python
results = [("Herons", 83), ("Otters", 79), ("Swifts", 83)]
print(sorted(results, key=lambda row: row[1]))
# [('Otters', 79), ('Herons', 83), ('Swifts', 83)]

Understanding how sorting algorithms work still matters: it explains stability, helps you choose keys for multi-level ordering, and is a staple of interviews. This lesson covers key functions and stability, implements insertion sort and merge sort, compares their costs, and shows the Python tools to use in real code. The examples rank crews in a rowing regatta.

Ordering keys

A key function takes one item and returns the value to compare. Python calls it once per item:

Python
crews = [("Herons", 83.4, "B"), ("Otters", 79.1, "A"), ("Kites", 91.0, "A")]
by_time = sorted(crews, key=lambda c: c[1])                 # fastest first
slowest_first = sorted(crews, key=lambda c: c[1], reverse=True)
by_division_then_time = sorted(crews, key=lambda c: (c[2], c[1]))

Returning a tuple sorts by several fields: first by division, then by time within each division. operator.itemgetter(1) and operator.attrgetter("time") are faster, readable alternatives to simple lambdas.

The sort is only as correct as its key. Sorting regatta results by crew name produces a perfectly sorted list that answers the wrong question. Watch for keys that compare in an unexpected way, such as times stored as text: "9:58" > "10:02" because strings compare character by character. Convert to numbers first, or use a key like key=lambda s: tuple(map(int, s.split(":"))).

Stability

A sort is stable if items with equal keys keep their original relative order. Python's sort is guaranteed stable:

Python
results = [("Herons", 83), ("Otters", 79), ("Swifts", 83)]
print(sorted(results, key=lambda r: r[1]))
# Herons stays before Swifts: both 83, Herons came first in the input

Stability lets you build multi-level sorts in passes: sort by the secondary key first, then by the primary key, and ties in the primary key stay ordered by the secondary one. Sorting by a tuple key achieves the same in one pass.

Stability doesn't decide ties for you. If the input order was arbitrary, such as the order crews registered, the tie order is arbitrary too. When ties matter, as they do for medals, add an explicit tie-breaker to the key, such as the final split time or lane number, so the ranking is deterministic and explainable.

Algorithm tradeoffs

Insertion sort builds a sorted prefix, taking each new item and shifting larger items right until it fits:

Python
def insertion_sort(items):
    items = list(items)
    for i in range(1, len(items)):
        current = items[i]
        j = i - 1
        while j >= 0 and items[j] > current:
            items[j + 1] = items[j]
            j -= 1
        items[j + 1] = current
    return items

It's O(n²) in the worst case (reversed input) but O(n) on nearly sorted data, uses O(1) extra space and is stable.

Merge sort splits the list in half, sorts each half recursively, and merges the two sorted halves by repeatedly taking the smaller front item:

Python
def merge_sort(items):
    if len(items) <= 1:
        return list(items)
    middle = len(items) // 2
    left, right = merge_sort(items[:middle]), merge_sort(items[middle:])
    merged, i, j = [], 0, 0
    while i < len(left) and j < len(right):
        if left[i] <= right[j]:          # <= keeps it stable
            merged.append(left[i]); i += 1
        else:
            merged.append(right[j]); j += 1
    return merged + left[i:] + right[j:]

It's O(n log n) in every case and stable, but needs O(n) extra space. Python's Timsort combines both ideas: it finds already-sorted runs, extends short ones with insertion sort and merges runs, which is why it's very fast on real-world data that's partly ordered.

Python tools

ToolReturnsChanges originalAccepts
sorted(iterable, key=, reverse=)new listnoany iterable
list.sort(key=, reverse=)Noneyeslists only
heapq.nsmallest(k, items, key=)k smallestnoany iterable
max() / min() with key=one itemnoany iterable
Python
podium = sorted(results, key=lambda r: r[1])[:3]
import heapq
fastest_three = heapq.nsmallest(3, results, key=lambda r: r[1])

Use sorted() when you need to keep the original order too, and list.sort() to sort a list you own in place. When you only need the top few items from a large collection, heapq.nsmallest and nlargest avoid sorting everything, and min()/max() find a single extreme in O(n).

Implementing your own sort is for learning; in application code the built-in is faster, stable and thoroughly tested. Your job is to supply the right key and tie-breakers.

Independent practice and a worked check

Task: trace insertion and merge sorting, then use a stable key-based sort for tied race results.

Python
results = [("Herons", 83, 3), ("Otters", 79, 1), ("Swifts", 83, 2), ("Kites", 91, 4)]

# Stable sort: ties keep registration order
print([name for name, t, lane in sorted(results, key=lambda r: r[1])])
# ['Otters', 'Herons', 'Swifts', 'Kites']

# Explicit tie-breaker: lower lane number wins a tie
print([name for name, t, lane in sorted(results, key=lambda r: (r[1], r[2]))])
# ['Otters', 'Swifts', 'Herons', 'Kites']

Trace merge sort on [83, 79, 83, 91] on paper: split into [83, 79] and [83, 91], sort each to [79, 83] and [83, 91], then merge to [79, 83, 83, 91], taking from the left half on ties. For the worked check, a stable sort preserves the original relative order of items with equal keys.

Reference and next practice

The Python Sorting HOW TO covers keys, stability and multi-level sorts. Continue with stacks and queues and revisit complexity. Practise with the Python exercises and Python quiz.

Sorting Algorithms Summary

  • Ordering keys: A key function defines the value used to compare records.
  • Stability: A stable sort retains relative order among equal key values.
  • Algorithm tradeoffs: Insertion sort illustrates local shifts while merge sort illustrates dividing and combining.
  • Python tools: sorted returns a new list while list.sort mutates the list, and Python sorting is stable.

Sorting Algorithms Examples

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

Example 1

Sorting Algorithms in a small program

Python
results = [("Herons", 83), ("Otters", 79), ("Swifts", 83)]
ordered = sorted(results, key=lambda row: row[1])
print(ordered)
Output
[('Otters', 79), ('Herons', 83), ('Swifts', 83)]
Line-by-line explanation
  1. Line 2The key picks the time from each tuple.
  2. Line 3Herons and Swifts both have 83; stability keeps Herons first because it came first in the input.
Example 2

Hand-written sorts against sorted()

Python
import random

def insertion_sort(items):
    items, shifts = list(items), 0
    for i in range(1, len(items)):
        current, j = items[i], i - 1
        while j >= 0 and items[j] > current:
            items[j + 1] = items[j]
            j -= 1
            shifts += 1
        items[j + 1] = current
    return items, shifts

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]:
            merged.append(left[i]); i += 1
        else:
            merged.append(right[j]); j += 1
    return merged + left[i:] + right[j:]

random.seed(4)
times = [random.randint(70, 99) for _ in range(200)]
by_insertion, shifts = insertion_sort(times)
print(by_insertion == merge_sort(times) == sorted(times))
print("shifts on random data:", shifts)
print("shifts on sorted data:", insertion_sort(sorted(times))[1])
print("shifts on reversed data:", insertion_sort(sorted(times, reverse=True))[1])
Output
True
shifts on random data: 10094
shifts on sorted data: 0
shifts on reversed data: 19245
Line-by-line explanation
  1. Lines 27 to 28All three methods agree on the result; random.seed(4) makes the data repeatable.
  2. Lines 29 to 31Insertion sort does no shifting on sorted input and the most on reversed input, illustrating its O(n) best case and O(n²) worst case.

Common Sorting Algorithms Mistakes

Sorting by the wrong key

Don't
times = ["10:02", "9:58", "11:15"]
print(sorted(times))        # ['10:02', '11:15', '9:58']: text order
Do
times = ["10:02", "9:58", "11:15"]
print(sorted(times, key=lambda t: tuple(map(int, t.split(":")))))
# ['9:58', '10:02', '11:15']

Assigning the result of list.sort()

Don't
results = results.sort(key=lambda r: r[1])   # results is now None
Do
results.sort(key=lambda r: r[1])             # sorts in place
ranked = sorted(results, key=lambda r: r[1]) # or keep the original

Sorting Algorithms in Real Projects

Sorting in real applications

Sorting runs underneath leaderboards, search results, reports and pagination. Databases sort with ORDER BY far more efficiently than fetching everything into Python, so large datasets are usually sorted in the query. In Python, sorting records by a tuple key with explicit tie-breakers gives reproducible rankings, which matters for anything people check, such as results tables and prize lists.

Merge sort's divide-and-combine pattern reappears in external sorting of files larger than memory and in merging sorted streams (heapq.merge). Interviewers ask about these algorithms because they test recursion, invariants and complexity reasoning at once.

Sorting Algorithms FAQ

What sorting algorithm does Python use?

Timsort, a hybrid of merge sort and insertion sort designed for real-world data. It's stable, O(n log n) in the worst case and O(n) on already sorted input. (CPython 3.11 refined its merge strategy, but the guarantees are unchanged.)

How do I sort a list of tuples by the second element?

sorted(items, key=lambda t: t[1]), or key=operator.itemgetter(1). Add reverse=True for descending order, and return a tuple such as (t[1], t[0]) from the key to break ties.

What does stable sorting mean?

Items that compare equal keep the order they had in the input. Python's sort is guaranteed stable, so you can sort by one key and then another, and earlier orderings survive among ties.

Is merge sort better than insertion sort?

For large or random data, yes: merge sort is O(n log n) versus insertion sort's O(n²). Insertion sort is simpler, uses no extra memory and is fast on small or nearly sorted lists, which is why Timsort uses it for short runs.

How do I sort in descending order but break ties ascending?

Negate numeric parts of the key: sorted(rows, key=lambda r: (-r.score, r.name)) sorts scores high to low and names A to Z within a score. For non-numeric fields, sort in two stable passes instead.

Sorting Algorithms Exercises

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

Multiple choiceeasy

What does a stable sort preserve for equal keys?

Multiple choicemedium

Which review concern is valid for sorting algorithms?

Sorting Algorithms Quiz

2 questions to check what you learned.

  1. What does a stable sort preserve for equal keys?

    • Reverse order
    • Their original relative order
    • Python ignores the distinction
  2. Which statement correctly describes algorithm tradeoffs?

    • Insertion sort illustrates local shifts while merge sort illustrates dividing and combining.
    • Only the example output matters, not the rule
    • Every operation has the same behavior