How Quicksort Works
A random pivot, a partition into lesser and greater, two recursive calls. The 1959 algorithm that still drives most stdlib sorts.
What you will learn
- Why a random pivot avoids the O(n squared) worst case on already-sorted input
- How the partition step splits a list into lesser-or-equal and greater groups in one pass
- How the base case at length less than 2 stops the recursion
- How the list-spread return stitches sorted halves around the pivot in O(n)
- Why the doctest covers the empty list, a typical case, and negatives as the contract
- How the __main__ block makes the file runnable as a standalone script
- Where this implementation diverges from production sorts that use median-of-three pivots and in-place partitioning
Prerequisites
- Comfort reading Python list comprehensions
- Familiarity with recursion and the call stack
- No prior knowledge of quicksort required
The story behind quicksort
Tony Hoare invented quicksort in 1959 while he was a graduate student on a British Council exchange at Moscow State University. His task was to sort Russian words for a Russian-to-English machine-translation project, and the existing options were too slow on the limited memory the prototype computer offered. The partition-then-recurse insight he sketched at the time turned what looked like an O(n squared) problem into a method that runs in O(n log n) on average.
Hoare published the algorithm formally in Communications of the ACM in 1961 as a 30-line Algol procedure called Quicksort. Sixty-five years later it is still the default sort in C's qsort, in the JVM's Arrays.sort for primitives, and in Rust's slice::sort_unstable. Python's list.sort uses Timsort, which folded quicksort's partition idea into a hybrid with merge sort. The reason is the one Hoare gave on the first page of the paper: pick a pivot, split, recurse, and the work falls out almost free.
Module Docstring: How to Run This File
sorts/quick_sort.py:1Before reading the code, the maintainers tell you how to drive the file: one command for doctests, one for manual input.
A reader needs to know how to execute the file before reading it, and the module docstring exists for exactly that. The first line names the algorithm and the implementation language so a grep over the catalog returns the right hit. The next two stanzas give the two ways to run the code: python3 -m doctest -v quick_sort.py walks every >>> example in the function docstring and prints which ones passed, and python3 quick_sort.py drops into the __main__ block at the bottom of the file for an interactive sort. Every algorithm file in the repository carries a docstring of this same shape because the contribution checklist makes it part of the gate.
Module docstrings in this repo are operational. They name the algorithm and give the two run commands a reviewer or learner needs.
"""
A pure Python implementation of the quick sort algorithm
For doctests run following command:
python3 -m doctest -v quick_sort.py
For manual testing run:
python3 quick_sort.py
"""
The Function Signature and the Base Case
sorts/quick_sort.py:16Recursion needs a stopping condition. A list of zero or one items is already sorted, so the function returns it unchanged.
Recursive functions that forget their base case run until the call stack overflows. Quicksort's base case is the cheapest one available: any list with fewer than two items is already sorted by definition, so the function returns it untouched. The check on lines 31 and 32 happens before any work, before the random pivot is even chosen, and it is what stops every recursive branch in the tree. The signature itself is plain: a list in, a list out, no Comparable bound and no generics. That keeps the doctests readable and the implementation focused on the algorithm rather than the type system. Each recursive call eventually hits a list of length zero or one and unwinds.
The base case at len(collection) < 2 is the floor of the recursion. Without it the function recurses forever on any non-empty input.
def quick_sort(collection: list) -> list:
"""A pure Python implementation of quicksort algorithm.
:param collection: a mutable collection of comparable items
:return: the same collection ordered in ascending order
Examples:
>>> quick_sort([0, 5, 3, 2, 2])
[0, 2, 2, 3, 5]
>>> quick_sort([])
[]
>>> quick_sort([-2, 5, 0, -45])
[-45, -2, 0, 5]
"""
# Base case: if the collection has 0 or 1 elements, it is already sorted
if len(collection) < 2:
return collection
Pivot Selection and the Partition
sorts/quick_sort.py:34A random pivot avoids the worst case on already-sorted input. Two list comprehensions partition the rest into lesser and greater.
Quicksort's average performance depends on the pivot landing somewhere near the median. A naive choice (always the first element) collapses to O(n squared) on already-sorted input because every partition produces one empty side and one full side. randrange(len(collection)) on line 35 picks a random index instead, which gives an expected balanced split across all input distributions. After the random pick, the pivot is removed from the list with pop so it does not appear in either half. Two list comprehensions on lines 39 and 40 then walk the remaining elements once each and sort them into lesser and greater by comparing against the pivot. Partitioning therefore costs O(n) per recursion level.
A random pivot defeats the sorted-input worst case. Two list comprehensions then partition the rest in one O(n) pass each.
# Randomly select a pivot index and remove the pivot element from the collection
pivot_index = randrange(len(collection))
pivot = collection.pop(pivot_index)
# Partition the remaining elements into two groups: lesser or equal, and greater
lesser = [item for item in collection if item <= pivot]
greater = [item for item in collection if item > pivot]
The Recursive Return Stitches the Result
sorts/quick_sort.py:42Sort the lesser half, sort the greater half, splice the pivot in the middle. The list-spread syntax does the splicing in one expression.
The recursive return is the line that turns partitioning into sorting. quick_sort(lesser) recursively sorts every element less than or equal to the pivot, quick_sort(greater) does the same for the strictly greater elements, and the list-spread syntax on line 43 unpacks both sorted halves into a new list with the pivot dropped between them. Because every element in lesser is at most the pivot and every element in greater is strictly above it, that concatenation is itself sorted. The recursion tree has depth O(log n) on average, and each level does O(n) partition work, which is where the O(n log n) average complexity comes from. The pivot only appears once in the output because it was popped from the input before the recursion fired.
List-spread concatenation glues the sorted halves around the pivot. The recursion does the rest, with O(log n) depth on average.
# Recursively sort the lesser and greater groups, and combine with the pivot
return [*quick_sort(lesser), pivot, *quick_sort(greater)]
The Doctest Is the Spec
sorts/quick_sort.py:17Three doctests pin the contract: a typical mixed-integer input, the empty list, and a list with negative values.
The function docstring is also the test suite. CI runs pytest --doctest-modules, which parses every >>> line in the file, executes the expression, and asserts the next line matches the actual output. Three examples appear here. quick_sort([0, 5, 3, 2, 2]) proves the sort handles duplicates and produces ascending order. quick_sort([]) proves the base case returns the empty list rather than raising. quick_sort([-2, 5, 0, -45]) proves negative numbers and zero work the same as positive ones. A reviewer reading these six lines knows the function's contract. A future change that breaks any of these examples fails the build before the PR can merge.
Doctests double as documentation and tests. These three examples pin the contract: ascending output, empty input handled, negatives supported.
"""A pure Python implementation of quicksort algorithm.
:param collection: a mutable collection of comparable items
:return: the same collection ordered in ascending order
Examples:
>>> quick_sort([0, 5, 3, 2, 2])
[0, 2, 2, 3, 5]
>>> quick_sort([])
[]
>>> quick_sort([-2, 5, 0, -45])
[-45, -2, 0, 5]
"""
The __main__ Block: Standalone Driver
sorts/quick_sort.py:46The __main__ guard makes the file runnable as a script. It reads comma-separated integers from stdin and prints the sorted result.
A self-contained algorithm file needs a way to be exercised by hand, and the __main__ guard is the convention every algorithm file in the repository follows. Importing this module from another file does not run the block, because the __name__ check is only true when Python is invoked directly with the file as its argument. The driver itself reads a comma-separated string from stdin, splits it, converts each token with int(), and prints the sorted list. The contribution guide bans bare input() calls without strip() because trailing whitespace would break the integer parse, which is why .strip() appears on line 48. Compare with sorts/insertion_sort.py for the same pattern with an additional doctest.testmod() call.
The __main__ guard runs only when the file is invoked directly. It reads stdin, sorts, and prints, which is enough to exercise the function without writing a test file.
if __name__ == "__main__":
# Get user input and convert it into a list of integers
user_input = input("Enter numbers separated by a comma:\n").strip()
unsorted = [int(item) for item in user_input.split(",")]
# Print the result of sorting the user-provided list
print(quick_sort(unsorted))
You've walked through 6 key areas of the The Algorithms - Python codebase.
Continue: How Heapsort Works → Browse all projectsCreate code tours for your project
Intraview lets AI create interactive walkthroughs of any codebase. Install the free VS Code extension and generate your first tour in minutes.
Install Intraview Free