The Algorithms - Python Python TheAlgorithms/Python

How Bubble Sort Works

Compare adjacent elements, swap if out of order, repeat. The algorithm that appears in every first-year course because the swapping motion is easy to picture.

6 stops ~14 min Verified 2026-05-05
What you will learn
  • Why the name bubble sort comes from the way large values migrate toward the end of the list
  • How the outer loop shrinks the unsorted region on each pass and why the inner loop only runs to i
  • How the early-exit flag converts O(n squared) average to O(n) on already-sorted input
  • Why bubble sort is stable and what that means for equal elements in the output
  • How the recursive variant encodes the same logic with a base-case return instead of a loop counter
  • How the __main__ benchmark quantifies the iterative versus recursive trade-off with timeit
Prerequisites
  • Comfort reading nested Python for loops
  • No prior knowledge of bubble sort required
The story behind bubble sort

The name bubble sort first appeared in print in 1956 in a paper by Friend in Journal of the ACM, though the algorithm itself was described in various forms before that. It became the standard first sorting algorithm taught in computer science courses because the motion is easy to picture: a large value at the left end of a list bubbles rightward one position per pass, like an air bubble rising through water.

What makes bubble sort pedagogically central is not its performance but its simplicity. The inner loop compares each adjacent pair and swaps them if they are out of order. One full pass guarantees that the largest unsorted element ends up at the back. Repeat n times and the list is sorted. That reasoning is direct enough to trace by hand on a five-element example in under a minute.

The algorithm's O(n squared) average case makes it unsuitable for production use. The early-exit optimization (the swapped flag you see in this file) brings it to O(n) on already-sorted input, which is its only practical advantage. Python's built-in sorted() uses Timsort and is faster for every realistic input shape. Bubble sort's value is in the classroom, not the codebase.

1 / 6

bubble_sort_iterative: Signature and the Doctest Battery

sorts/bubble_sort.py:4

The function takes any ordered collection with comparable items. Seventeen doctests cover duplicates, empty, negatives, strings, floats, and random samples.

The doctest battery here is the most extensive in the sorts/ directory. Most sort files test three or four input shapes; this one tests thirteen. The reason is coverage: bubble sort's correctness claim includes stability (equal elements stay in original order), and the doctest on line 22 verifying [3, 3, 3, 3] pins that. The already-sorted test on line 20 verifies the early-exit behavior does not corrupt a list that needed no swaps. String and float inputs prove that Any in the type hint is real: the function works on any type whose > operator is defined. The two random 100-element samples at the end (one of integers, one of mixed ASCII) are what the insertion sort doctest uses too, and they function as a statistical sanity check against Python's built-in sorted().

Key takeaway

Seventeen doctests cover stability, already-sorted early exit, strings, floats, and random samples. Each additional case pins a property the two-sentence description alone cannot verify.

def bubble_sort_iterative(collection: list[Any]) -> list[Any]:
    """Pure implementation of bubble sort algorithm in Python

    :param collection: some mutable ordered collection with heterogeneous
    comparable items inside
    :return: the same collection ordered in ascending order

    Examples:
    >>> bubble_sort_iterative([0, 5, 2, 3, 2])
    [0, 2, 2, 3, 5]
    >>> bubble_sort_iterative([])
    []
    >>> bubble_sort_iterative([-2, -45, -5])
    [-45, -5, -2]
    >>> bubble_sort_iterative([-23, 0, 6, -4, 34])
    [-23, -4, 0, 6, 34]
    >>> bubble_sort_iterative([1, 2, 3, 4])
    [1, 2, 3, 4]
    >>> bubble_sort_iterative([3, 3, 3, 3])
    [3, 3, 3, 3]
    >>> bubble_sort_iterative([56])
    [56]
    >>> bubble_sort_iterative([0, 5, 2, 3, 2]) == sorted([0, 5, 2, 3, 2])
    True
    >>> bubble_sort_iterative([]) == sorted([])
    True
    >>> bubble_sort_iterative([-2, -45, -5]) == sorted([-2, -45, -5])
    True
    >>> bubble_sort_iterative([-23, 0, 6, -4, 34]) == sorted([-23, 0, 6, -4, 34])
    True
    >>> bubble_sort_iterative(['d', 'a', 'b', 'e']) == sorted(['d', 'a', 'b', 'e'])
    True
    >>> bubble_sort_iterative(['z', 'a', 'y', 'b', 'x', 'c'])
    ['a', 'b', 'c', 'x', 'y', 'z']
    >>> bubble_sort_iterative([1.1, 3.3, 5.5, 7.7, 2.2, 4.4, 6.6])
    [1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7]
    >>> bubble_sort_iterative([1, 3.3, 5, 7.7, 2, 4.4, 6])
    [1, 2, 3.3, 4.4, 5, 6, 7.7]
    >>> import random
    >>> collection_arg = random.sample(range(-50, 50), 100)
    >>> bubble_sort_iterative(collection_arg) == sorted(collection_arg)
    True
    >>> import string
    >>> collection_arg = random.choices(string.ascii_letters + string.digits, k=100)
    >>> bubble_sort_iterative(collection_arg) == sorted(collection_arg)
    True
    """
2 / 6

The Iterative Body and the Early-Exit Flag

sorts/bubble_sort.py:51

The outer loop shrinks pass length each round. The swapped flag exits early after a no-swap pass, reducing best-case cost to O(n).

The outer loop calls reversed(range(length)), which iterates i from length-1 down to 0. The inner loop runs from j=0 to j=i-1, which means each outer pass considers one fewer comparison than the last: after the first pass, the largest element is guaranteed to be in its final position, so the inner loop can skip it. That shrinking bound takes the comparison count from n squared to n(n-1)/2, but the asymptotic class stays O(n squared). The swapped flag on lines 53 and 58 is what achieves the O(n) best case. If a full inner pass makes no swaps, the list is already sorted and the break on line 59 exits immediately. The already-sorted doctest on line 20 verifies that this path executes correctly.

Key takeaway

The swapped flag exits after one no-swap pass, making already-sorted input O(n). Without it, the algorithm always does O(n squared) comparisons.

    length = len(collection)
    for i in reversed(range(length)):
        swapped = False
        for j in range(i):
            if collection[j] > collection[j + 1]:
                swapped = True
                collection[j], collection[j + 1] = collection[j + 1], collection[j]
        if not swapped:
            break  # Stop iteration if the collection is sorted.
    return collection
3 / 6

bubble_sort_recursive: The Same Logic Without a Loop Counter

sorts/bubble_sort.py:63

The recursive variant runs one forward pass per call and recurses only if a swap occurred. The base case is an implicit no-swap return.

The recursive variant is a different encoding of the same algorithm. Where the iterative version uses a break to exit early, this version uses the conditional at the tail: return collection if not swapped else bubble_sort_recursive(collection). One full forward pass runs in the body (lines 107 through 110), and the result of that pass determines whether another pass is needed. The function recurses on the same collection object, not a copy, so the list is modified in place across all recursive frames. The doctest battery mirrors the iterative version with one addition: line 94 tests a mixed-case ASCII list (['a', 'Z', 'B', 'C', 'A', 'c']) and shows that Python sorts uppercase letters before lowercase, which is a fact about ord() values rather than anything bubble sort does.

Key takeaway

The recursive version replaces the outer loop with a tail call. The early-exit condition is the same swapped flag, but expressed as a conditional return instead of a break.

def bubble_sort_recursive(collection: list[Any]) -> list[Any]:
    """It is similar iterative bubble sort but recursive.

    :param collection: mutable ordered sequence of elements
    :return: the same list in ascending order

    Examples:
    >>> bubble_sort_recursive([0, 5, 2, 3, 2])
    [0, 2, 2, 3, 5]
    >>> bubble_sort_recursive([])
    []
    >>> bubble_sort_recursive([-2, -45, -5])
    [-45, -5, -2]
    >>> bubble_sort_recursive([-23, 0, 6, -4, 34])
    [-23, -4, 0, 6, 34]
    >>> bubble_sort_recursive([0, 5, 2, 3, 2]) == sorted([0, 5, 2, 3, 2])
    True
    >>> bubble_sort_recursive([]) == sorted([])
    True
    >>> bubble_sort_recursive([-2, -45, -5]) == sorted([-2, -45, -5])
    True
    >>> bubble_sort_recursive([-23, 0, 6, -4, 34]) == sorted([-23, 0, 6, -4, 34])
    True
    >>> bubble_sort_recursive(['d', 'a', 'b', 'e']) == sorted(['d', 'a', 'b', 'e'])
    True
    >>> bubble_sort_recursive(['z', 'a', 'y', 'b', 'x', 'c'])
    ['a', 'b', 'c', 'x', 'y', 'z']
    >>> bubble_sort_recursive([1.1, 3.3, 5.5, 7.7, 2.2, 4.4, 6.6])
    [1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7]
    >>> bubble_sort_recursive([1, 3.3, 5, 7.7, 2, 4.4, 6])
    [1, 2, 3.3, 4.4, 5, 6, 7.7]
    >>> bubble_sort_recursive(['a', 'Z', 'B', 'C', 'A', 'c'])
    ['A', 'B', 'C', 'Z', 'a', 'c']
    >>> import random
    >>> collection_arg = random.sample(range(-50, 50), 100)
    >>> bubble_sort_recursive(collection_arg) == sorted(collection_arg)
    True
    >>> import string
    >>> collection_arg = random.choices(string.ascii_letters + string.digits, k=100)
    >>> bubble_sort_recursive(collection_arg) == sorted(collection_arg)
    True
    """
4 / 6

The Recursive Loop Body

sorts/bubble_sort.py:105

One forward pass over the collection, swapping adjacent elements if out of order, then returning the result or recursing.

The forward pass on lines 107 through 110 always scans the full length - 1 adjacent pairs, regardless of how many swaps the previous call made. That is the structural difference from the iterative version, which shrinks the inner loop on each outer pass. The recursive version does more comparison work per level because it does not track how far the sorted suffix extends. In exchange, the code is six lines instead of ten, and the stopping condition is a single expression on line 112. The benchmark in the __main__ block below quantifies the cost of that trade-off with timeit over 10,000 runs. On random input, the iterative version is typically faster because fewer comparisons are needed in the final passes.

Key takeaway

The recursive body always scans all length-1 pairs. The iterative version's shrinking inner bound does fewer comparisons per pass; the benchmark in __main__ measures the gap.

    length = len(collection)
    swapped = False
    for i in range(length - 1):
        if collection[i] > collection[i + 1]:
            collection[i], collection[i + 1] = collection[i + 1], collection[i]
            swapped = True

    return collection if not swapped else bubble_sort_recursive(collection)
5 / 6

The __main__ Block: doctest and timeit Benchmark

sorts/bubble_sort.py:115

Direct execution validates doctests with doctest.testmod(), then benchmarks iterative vs recursive over 10,000 runs using timeit.

Most algorithm files in this repository end with a simple read-sort-print driver. This one adds a timeit benchmark. The benchmark passes unsorted[:] rather than unsorted to the sort function inside the timed string, because both sort functions mutate the collection in place and a reused sorted list would make every subsequent sort trivially fast. The slice copy is the correct control. Running python3 sorts/bubble_sort.py prints both sorted results and their timing across 10,000 runs, confirming the comment on line 122: iterative is slightly faster because it does fewer total comparisons. The imports of sample and timeit are inside the if __name__ block to avoid loading them when the module is imported by another file or by pytest.

Key takeaway

Passing unsorted[:] to the timed call is the correct control for in-place sorts. Without the copy, each run after the first starts with an already-sorted list.

if __name__ == "__main__":
    import doctest
    from random import sample
    from timeit import timeit

    doctest.testmod()

    # Benchmark: Iterative seems slightly faster than recursive.
    num_runs = 10_000
    unsorted = sample(range(-50, 50), 100)
    timer_iterative = timeit(
        "bubble_sort_iterative(unsorted[:])", globals=globals(), number=num_runs
    )
    print("\nIterative bubble sort:")
    print(*bubble_sort_iterative(unsorted), sep=",")
    print(f"Processing time (iterative): {timer_iterative:.5f}s for {num_runs:,} runs")

    unsorted = sample(range(-50, 50), 100)
    timer_recursive = timeit(
        "bubble_sort_recursive(unsorted[:])", globals=globals(), number=num_runs
    )
    print("\nRecursive bubble sort:")
    print(*bubble_sort_recursive(unsorted), sep=",")
    print(f"Processing time (recursive): {timer_recursive:.5f}s for {num_runs:,} runs")
6 / 6

Why Two Implementations in One File

sorts/bubble_sort.py:4

The file holds iterative and recursive variants side by side to make the structural correspondence visible and to let the benchmark quantify the trade-off.

Most files in sorts/ hold exactly one function. The contribution guide says identical reimplementations of an existing file are rejected, but variants that encode the same algorithm differently add value. The iterative and recursive forms of bubble sort make the same logical steps visible from two angles: one controls repetition with a loop counter and a break, the other with a tail call and a conditional return. Placing them side by side in the same file lets a reader compare them directly without switching files, and the timeit benchmark in __main__ gives a concrete measurement rather than a prose claim about which is faster. That pairing of two perspectives plus a benchmark is what separates this file from a single-function stub and justifies the larger doctest count. See the Sorting Algorithms category tour for the full set of variants across sorts/.

Key takeaway

Two variants in one file let readers compare iterative and recursive encodings directly. The benchmark turns the prose claim about speed into a measurement.

def bubble_sort_iterative(collection: list[Any]) -> list[Any]:
    """Pure implementation of bubble sort algorithm in Python

    :param collection: some mutable ordered collection with heterogeneous
    comparable items inside
    :return: the same collection ordered in ascending order
Your codebase next

Create 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