Symptom
Somebody proposes a tool that will find all the bugs. Not most bugs. All of them. The pitch has the shape: we enumerate the failure modes, we write a checker for each, we keep adding checkers, and the set of undetected bugs shrinks toward zero.
Or the version you have probably said yourself, over a beer, about a language: eventually the type system will be strong enough to reject every program that goes wrong.
Or the simplest one, which is a question rather than a claim: there are infinitely many possible programs, and infinitely many possible problems, so surely there is enough supply to meet demand?
There is not. There are strictly more problems than there are programs, and the gap is not a small one, and this is provable in about fifteen lines using nothing but a table and the ability to add one.
Statement
Cantor’s theorem has a general form, but the version that does the work here is concrete:
The set of infinite binary sequences is uncountable. No matter how you list such sequences $s_1, s_2, s_3, \dots$, some infinite binary sequence is missing from your list.
“Countable” means you can put the elements in a list indexed by $1, 2, 3, \dots$ — every element gets a finite position number, with no element left out. The integers are countable. The rationals are countable, which already surprises people. The infinite binary sequences are not.
The general statement, which costs nothing extra once you have the specific one:
For any set $S$, there is no surjection from $S$ onto its power set $\mathcal{P}(S)$. So $|S| < |\mathcal{P}(S)|$, always, including when $S$ is infinite.
Argument
Suppose someone hands you a list of infinite binary sequences and claims it contains every one of them. Write the list as a table, one sequence per row:
$$ \begin{array}{c|cccccc} & 1 & 2 & 3 & 4 & 5 & \cdots \\ \hline s_1 & \mathbf{0} & 1 & 1 & 0 & 1 & \cdots \\ s_2 & 1 & \mathbf{1} & 0 & 0 & 1 & \cdots \\ s_3 & 0 & 0 & \mathbf{0} & 1 & 1 & \cdots \\ s_4 & 1 & 0 & 1 & \mathbf{1} & 0 & \cdots \\ s_5 & 0 & 1 & 1 & 0 & \mathbf{0} & \cdots \\ \vdots & & & & & & \ddots \end{array} $$Now build a new sequence $d$ by walking down the diagonal — the bolded entries, $s_1$’s first bit, $s_2$’s second bit, $s_3$’s third — and flipping each one. In the table above the diagonal reads $0,1,0,1,0,\dots$, so
$$d = 1,0,1,0,1,\dots$$Ask the only question that matters: is $d$ somewhere in the list?
It is not $s_1$, because $d$ differs from $s_1$ in the first bit — we made it so. It is not $s_2$, because $d$ differs from $s_2$ in the second bit. In general $d$ is not $s_n$ for any $n$, because $d$ and $s_n$ disagree at position $n$ by construction. Two sequences that disagree anywhere are not the same sequence.
So $d$ is an infinite binary sequence not in the list. The list was not complete. And this holds for every list, since we never assumed anything about which sequences were in which rows. Therefore no complete list exists.
That is the whole proof. Note the shape, because it recurs: we did not find a sequence that lists have trouble with. We built, from any given list, a specific counterexample to that list’s completeness — a machine that eats an alleged solution and outputs its refutation.
Why this counts programs. A program is a finite string over a finite alphabet. Finite strings over a finite alphabet are countable: list all strings of length 1, then all of length 2, and so on, each block finite, every string eventually reached. So the programs are countable, in a list, with $n$-th entries.
Now consider decision problems on the naturals — functions $f : \mathbb{N} \to {0,1}$. Each such function is an infinite binary sequence. There are uncountably many of them. Countably many programs, uncountably many problems.
The counting is not close. Deleting the computable problems from the set of all problems removes so little that the remainder is still exactly as large as it started. Almost every function $\mathbb{N} \to {0,1}$ is not computed by anything, and this was known before anybody had built a computer.
Forbids
A complete enumeration of the reals. Not “we haven’t found one” — there is none, and this is where the argument was born.
A program that computes every function. Trivially, from the counting: the programs run out and the functions do not.
A sound and complete finite checker for every semantic property. Any analysis tool is a program, so tools are countable; the properties they might need to decide are not. This is a counting argument, so it forbids only in aggregate — it says the tools cannot cover everything, without naming a single problem they miss. Naming one takes the diagonal argument applied to machines rather than to sequences, which is the halting problem, and which is the next post.
Any claim of the form “our set is the same size as its power set.” Cantor’s general theorem kills that for every $S$, finite or infinite. There is no largest infinity; $\mathcal{P}$ always escapes.
Does not forbid
It does not mean “we can’t reach the missing sequence.” The diagonal $d$ is not mysterious or unreachable. Given the list, $d$ is completely specified — you can compute any bit of it you like, provided you can compute bits of the listed sequences. The proof is a construction, not an appeal to the unknown. This matters because the mystical reading (“some truths are forever beyond us”) is both the popular one and the wrong one.
It does not say that most problems you will face are unsolvable, and this is the misreading that does real damage. The inference runs: almost all functions are uncomputable, therefore expecting to solve an arbitrary hard problem is naive. It gets deployed against real projects — the SMT-solver-based verifier, the automatic scheduler, the program-synthesis tool — as if the counting argument had ranged over the problems that occur in practice. It has not. The uncountably many functions are, essentially all of them, infinite tables of bits with no finite description. A problem that arrives in a ticket has a finite description by construction, which places it in the countable sliver before anyone starts work. Cantor says the supply of describable problems cannot cover the space of all functions. It says nothing about whether your problem is in the covered part, and the base rate there is far better than the theorem’s arithmetic suggests. Z3 solves NP-hard instances daily.
It does not forbid deciding a property for the programs you actually have. Uncountability is a statement about an infinite space. Your codebase is finite. “Does this specific 200-line function terminate?” is very often answerable, and a termination checker that succeeds on 90% of real loops and says “don’t know” on the rest is not defeated by Cantor. Rejecting a tool because a theorem forbids its perfect version is how you end up with no tool.
It does not say the uncomputable functions are weird. This inverts the truth and it is worth stating plainly. The computable functions are the vanishingly rare special case — a measure-zero sliver. Every function you have ever written down lives in that sliver, which is why the sliver feels like the whole world. “Almost all functions are uncomputable” is not a statement about pathology; it is a statement about how extraordinarily special the things we can name are.
It does not require the axiom of choice, or any set theory beyond a table. People who half-remember this argument sometimes suspect a trick in the foundations. There is no trick. The proof uses one list, one diagonal, and one negation.
Boundary
The result is exact — $|S| < |\mathcal{P}(S)|$ with no slack — so the interesting territory is not weakening it but redirecting it.
- Diagonalization against machines rather than sequences. Rows become programs, and the diagonal entry becomes “what does program $n$ do on input $n$.” Flipping that gives a program that disagrees with every program on at least one input, which is the halting problem (T010) and, with a change of vocabulary, Gödel’s first incompleteness theorem (T015). Same table, same flip, different labels on the rows. That is the reason this post exists this early: the technique is worth more than the theorem.
- Diagonalization inside complexity. Run the diagonal with a clock on it and you separate complexity classes: the time hierarchy theorem says more time strictly buys more computable problems. The same trick, resource-bounded.
- Where diagonalization stops working. It is not a universal solvent, and knowing where it fails is the mark of understanding it. Relativization (Baker–Gill–Solovay) shows that any argument that survives adding an oracle cannot settle P vs NP, and plain diagonalization survives oracles. That is why fifty years of diagonalizing has not resolved the field’s central question, and why the techniques that have made progress look nothing like this one.
- Constructive readings. In constructive mathematics the diagonal argument survives essentially intact, which is unusual for a proof by contradiction — because it is not really one. It is a construction with a contradiction stapled to the end for presentation. Strip the staple and it still builds $d$.