Skip to content
The Lumenary

Math laboratory

Cantor's diagonal trick on a finite list

Builds the diagonal-flipped sequence for a fixed list of 8 infinite binary sequences and checks exactly that it differs from every listed sequence. The finite run illustrates the mechanism only; the proof that the infinite binary sequences cannot be listed is Cantor's general argument.

Back to the labExact computation
Cantor's diagonal trick on a finite listPythonExact computationComputationally tested
"""Cantor's diagonal trick, run on a finite list.

An infinite binary sequence is a rule giving a digit 0 or 1 at every position
n = 0, 1, 2, ... Here each sequence is a Python function of n. Given a list of
sequences s_0, s_1, s_2, ..., Cantor builds a new sequence d by flipping the
diagonal: d(n) = 1 - s_n(n). Then d differs from s_n at position n, so d is
not anywhere in the list.

We run the mechanism on a finite list of 8 sequences. The finite run only
illustrates the trick; the proof that no list of infinite sequences can be
complete is the general argument, which a computer cannot run to the end.

Runs unchanged in the browser (Pyodide) using only the standard library.
"""

from itertools import product
from math import isqrt
import textwrap

def _is_prime(n):
    """Trial-division primality test (n >= 0)."""
    if n < 2:
        return False
    return all(n % d for d in range(2, isqrt(n) + 1))

def _sqrt2_digit(n):
    """Digit n (counting from 0) after the binary point of sqrt(2) = 1.0110101..."""
    return isqrt(2 << (2 * (n + 1))) & 1

#: A fixed, deterministic list of infinite binary sequences: (name, rule).
SEQUENCES = [
    ("all zeros", lambda n: 0),
    ("all ones", lambda n: 1),
    ("alternating", lambda n: n % 2),
    ("1 at squares", lambda n: int(isqrt(n) ** 2 == n)),
    ("1 at primes", lambda n: int(_is_prime(n))),
    ("Thue-Morse", lambda n: bin(n).count("1") % 2),
    ("1 at multiples of 3", lambda n: int(n % 3 == 0)),
    ("binary digits of sqrt 2", _sqrt2_digit),
]

def prefix(seq, length):
    """The first ``length`` digits of a sequence, as a list."""
    return [seq(n) for n in range(length)]

def diagonal_flip(seqs):
    """Return Cantor's diagonal sequence d with d(n) = 1 - seqs[n](n).

    ``seqs`` is a list of sequences (functions of n). For a finite list, d(n) is
    only defined for n < len(seqs); in the infinite argument the list never
    runs out, so d is defined everywhere.
    """
    seqs = list(seqs)

    def d(n):
        if not 0 <= n < len(seqs):
            raise IndexError("the diagonal is only defined at positions 0..len(list)-1")
        return 1 - seqs[n](n)

    return d

def differs_from_every_row(seqs, d):
    """True when d(i) != seqs[i](i) for every i - so d equals none of the listed sequences."""
    return all(d(i) != s(i) for i, s in enumerate(seqs))

def first_difference(a, b, length):
    """The first position n < length where sequences a and b differ, or None."""
    for n in range(length):
        if a(n) != b(n):
            return n
    return None

def diagonal_flip_rows(rows):
    """Diagonal trick on finite rows of digits: return [1 - rows[i][i] for each i].

    Raises ValueError when some row is shorter than the number of rows - the
    trick needs one position per row.
    """
    if any(len(row) < len(rows) for row in rows):
        raise ValueError("every row needs at least as many digits as there are rows")
    return [1 - row[i] for i, row in enumerate(rows)]

def show_row(digits, mark=None):
    """Digits separated by spaces, with the digit at position ``mark`` in [brackets]."""
    return "".join(f"[{x}]" if i == mark else f" {x} " for i, x in enumerate(digits))

def main():
    seqs = [rule for _, rule in SEQUENCES]
    width = 12
    print(f"A list of {len(seqs)} infinite binary sequences (first {width} digits shown).")
    print("The digit in [brackets] is row n, position n - the diagonal.")
    print()
    for i, (name, rule) in enumerate(SEQUENCES):
        print(f"  s{i} {name:<24}{show_row(prefix(rule, width), mark=i)} ...")
    d = diagonal_flip(seqs)
    diag = [d(n) for n in range(len(seqs))]
    print(f"  d  {'flip each [digit]':<24}{show_row(diag)}   (positions 0..{len(seqs) - 1})")
    print()
    print(f"d differs from s_n at position n, for every n in the list: {differs_from_every_row(seqs, d)}")
    firsts = [first_difference(d, s, len(seqs)) for s in seqs]
    print(f"First position where d and s0, s1, ... differ: {firsts}")
    print("(d may differ earlier than the diagonal; the diagonal guarantees a difference.)")
    reordered = list(reversed(seqs))
    d2 = diagonal_flip(reordered)
    print(f"Reverse the order of the list: new d = "
          f"{''.join(str(d2(n)) for n in range(len(seqs)))}, "
          f"still in no row: {differs_from_every_row(reordered, d2)}")
    print()
    rows = [list(bits) for bits in product((0, 1), repeat=3)]
    flipped = diagonal_flip_rows(rows[:3])
    where = rows.index(flipped)
    print("Finite caution: all 2^3 = 8 binary strings of length 3 CAN be listed.")
    print(f"Flipping the diagonal of the first 3 rows gives {''.join(map(str, flipped))}"
          f" - row {where} of that list:")
    print("the trick only beats one row per position, so it needs infinite sequences.")
    print()
    print(textwrap.fill(
        "Epistemic status: these checks are exact, but they only illustrate the "
        "mechanism on 8 sequences. The proof that no infinite list s0, s1, s2, ... "
        "contains every infinite binary sequence is Cantor's general argument, "
        "which applies to every such list at once; no program can check them all.",
        width=78,
    ))

if __name__ == "__main__":
    main()
Runs entirely in your browser, in an isolated worker. Nothing is sent to a server.

Used in