AnyLearn
All lessons

The Halting Problem and the Shape of the Proof

Once memory stops being the constraint, a different kind of limit appears. No program can decide whether an arbitrary program halts, and the proof fits in eight lines. This lesson builds that argument, separates deciding from merely recognising, and shows how one impossibility result propagates to every other question worth asking.

Updated · AI-authored, review-gated · how lessons are made

Not signed in: your progress and quiz score won't be saved.
Progress1 / 9

The question, stated carefully

Here is a service anyone would pay for. Given a program P and an input w, report whether P eventually stops on w, or runs forever. Call it halts(P, w).

The requirements are strict and worth naming, because every one of them is load bearing. The answer must be correct on every pair. It must be produced for every pair, including programs designed to be awkward. And halts must itself always terminate, otherwise it has not answered anything.

No such program exists. This is not a statement about current techniques or available compute. It is a proof that the specification is contradictory, in the same sense that no integer is both even and odd.

Turing proved it in the 1936 paper that introduced the machine, as a step toward answering Hilbert's Entscheidungsproblem in the negative.

Full lesson text

All 9 steps on one page, for reading, reference, and search.

Show

1. The question, stated carefully

Here is a service anyone would pay for. Given a program P and an input w, report whether P eventually stops on w, or runs forever. Call it halts(P, w).

The requirements are strict and worth naming, because every one of them is load bearing. The answer must be correct on every pair. It must be produced for every pair, including programs designed to be awkward. And halts must itself always terminate, otherwise it has not answered anything.

No such program exists. This is not a statement about current techniques or available compute. It is a proof that the specification is contradictory, in the same sense that no integer is both even and odd.

Turing proved it in the 1936 paper that introduced the machine, as a step toward answering Hilbert's Entscheidungsproblem in the negative.

2. The proof, written as a program

Assume halts exists. Then this program can be written, because everything in it is ordinary code:

def halts(program, arg):
    """Assumed: always returns, always correct."""
    ...

def paradox():
    if halts(paradox, None):   # ask about myself
        while True:            # ...then do the opposite
            pass
    return "done"

Now ask what halts(paradox, None) returns.

If it returns True, then paradox enters the infinite loop and never halts. The answer was wrong.

If it returns False, then paradox skips the loop and returns immediately. The answer was wrong again.

Both branches contradict the assumption that halts is correct. The only step that can be rejected is the assumption itself. Therefore halts does not exist.

3. Which ingredients the argument actually used

It is worth isolating what made that work, because the same two ingredients drive every result in this lesson.

The first is universality, established in the previous lesson: a program is data, so paradox can be passed a description of itself without any special mechanism. Self-reference is not a trick here, it is a consequence of stored-program computing.

The second is the ability to negate the answer. paradox does not merely consult halts, it consults it and then behaves in the opposite way. Any purported decider can be wrapped in this way.

Notice what the argument never uses: the size of the program, the speed of the machine, the amount of memory, or any detail of how halts works internally. That is why no better algorithm and no larger computer changes the conclusion.

4. Deciding is stronger than recognising

The halting problem is not entirely beyond reach, and the exact sense in which it is not is the most useful distinction in the subject.

A language is decidable if some machine always halts and answers correctly, yes or no. It is recognisable if some machine halts and says yes on every member, but is permitted to run forever on non-members.

Halting is recognisable. Simulate P on w and say yes the moment it stops. If P halts, this reports yes eventually. If P never halts, the simulation never halts either, so no answer is ever produced. That is allowed by recognisability and forbidden by decidability.

So the failure is precise. The problem is not that nothing can be learned about halting. It is that a program cannot distinguish not yet from never.

5. Three tiers, and what separates them

The tiers are not a vague ordering, they interlock exactly. A question is decidable precisely when both it and its opposite are recognisable: run the two searches side by side, one step each in turn, and whichever reports first gives the answer, so the combined procedure always terminates.

That criterion settles the last box immediately. Halting is recognisable and not decidable, so its complement cannot be recognisable, or the pairing above would decide it.

So "this program runs forever" is worse off than "this program halts". There is no procedure that reliably confirms an infinite loop even given unlimited time, whereas termination at least announces itself when it happens.

flowchart TD
A["A yes-or-no question about programs"] --> B["Decidable: always halts with the answer"]
A --> C["Recognisable: halts on yes, may run forever on no"]
A --> D["Not even recognisable"]
C --> E["Halting sits here"]
D --> F["Non-halting sits here"]
B --> G["Decidable exactly when it and its complement are both recognisable"]

6. Reduction: how one impossibility spreads

Nobody proves undecidability from scratch twice. The standard move is reduction: show that a decider for your problem X could be used to build a decider for halting. Since halting has none, X has none either.

Take X = "does this program ever print anything?" Suppose ever_prints(Q) existed. Then, given P and w, build:

def make_probe(P, w):
    def probe():
        run(P, w)          # no output of its own
        print("reached")   # only if run(P, w) returned
    return probe

def halts(P, w):
    return ever_prints(make_probe(P, w))

probe prints if and only if P halts on w. So ever_prints would decide halting. It cannot, so ever_prints does not exist.

The direction is the part people invert. To prove X hard, reduce the known-hard problem to X, not the other way around.

7. Rice's theorem generalises the whole exercise

Doing reductions one property at a time would be endless. H. G. Rice proved the general case in "Classes of Recursively Enumerable Sets and Their Decision Problems", Transactions of the American Mathematical Society, volume 74, issue 2, 1953, pages 358 to 366.

Rice's theorem: every non-trivial semantic property of programs is undecidable. Two words carry the weight. Semantic means the property depends only on what the program computes, not on how it is written. Non-trivial means at least one program has it and at least one does not.

The reach is total. Does this program compute the identity function? Does it ever dereference null? Is it equivalent to that other program? Does it always terminate? All semantic, all non-trivial, all undecidable.

Syntactic questions escape, because they are not about behaviour: line count, whether an identifier appears, whether the code parses.

8. Four things the halting problem does not say

The result is quoted far more often than it is stated correctly, and the misreadings all share a shape: they turn a claim about all programs into a claim about each program.

  • It does not say any particular program is undecidable. while True: pass obviously loops. print(1) obviously halts. Countless individual cases are settled trivially.
  • It does not say the question is hard for restricted languages. Remove unbounded loops and recursion and termination becomes decidable outright. The undecidability is a property of Turing-complete languages, not of programming.
  • It does not say approximation is impossible. A tool may answer halts, loops, or do not know. Only the third option is being outlawed as unnecessary.
  • It does not depend on resources. Bounded memory makes the state space finite and the question decidable in principle, at a cost so far beyond feasible that it buys nothing.

9. The map, and what it forces

QuestionStatus
Does this string parse as valid syntax?Decidable
Does this DFA accept any string at all?Decidable
Does program P halt on input w?Recognisable, not decidable
Does P halt on every input?Not even recognisable
Are P and Q equivalent?Undecidable, by Rice
Does P ever dereference null?Undecidable, by Rice

The top two rows are the ones tools rely on, and both are syntactic or finite-state. Everything below is semantic, and Rice's theorem closes the whole region at once.

This is not an academic boundary. Every type checker, linter, optimiser and verifier is trying to answer a question in the lower half of that table. Since none of them can, each has to choose which way to be wrong. The next lesson is about that choice, and about how much can still be delivered under it.

Check your understanding

The lesson ends with a 5-question quiz. Take it in the player above to see your score.

  1. In the diagonalisation proof, why does assuming `halts` is correct lead to a contradiction?
    • Because `paradox` consults `halts` about itself and then does the opposite of whatever it is told
    • Because `halts` cannot read its own source code
    • Because `paradox` requires unbounded memory to run
    • Because `halts` is too slow to finish before `paradox` starts
  2. Halting is recognisable but not decidable. What does the recognisable half mean concretely?
    • Halting can be decided for programs under a fixed size
    • Simulating P on w reports yes whenever P halts, but produces no answer when it does not
    • A machine can always report whether P loops, given enough memory
    • Halting can be decided if the input w is known in advance
  3. Why is 'this program runs forever' in a worse tier than 'this program halts'?
    • Infinite loops require more memory to detect
    • It is decidable only for deterministic programs
    • A question is decidable exactly when it and its complement are both recognisable, so the complement of a recognisable-but-undecidable question cannot be recognisable
    • It depends on the input, whereas halting does not
  4. To prove some new problem X undecidable by reduction, what must be shown?
    • That X can be solved using a decider for halting
    • That X and halting have the same running time
    • That X is recognisable but its complement is not
    • That a decider for X could be used to build a decider for halting
  5. Which question escapes Rice's theorem?
    • Does this program terminate on every input?
    • Does this source file contain more than 500 lines?
    • Is this program equivalent to that one?
    • Does this program ever dereference a null pointer?

Related lessons

Programming
advanced

Refs: A Branch Is a File Containing a Hash

Hashes are unusable by hand, so Git names them. A branch is not a copy of anything, not a directory, and not a container for commits: it is a forty-character file. Once that is clear, checkout, reset, detached HEAD and the reflog stop being separate concepts and become one operation on one kind of file.

8 steps·~12 min
Programming
intermediate

The Object Database: Git Is a Content-Addressed Store

Git is usually taught as a set of commands. Underneath it is a key-value store where the key is a hash of the content, and four object types built on that one idea. This lesson derives the hash by hand, shows what a commit actually contains, and explains why the design makes history tamper-evident.

8 steps·~12 min
Programming
advanced

The Design Philosophy: What Go Left Out

Go is unusual for what it refuses to include. Errors are values, not exceptions. Interfaces are satisfied without declaring it. Formatting is not a choice. This lesson works through the omissions, the reasoning behind each, and the real costs, because every one of them trades expressiveness for something else.

8 steps·~12 min
Programming
advanced

Channels: Share Memory by Communicating

Cheap concurrency without a coordination primitive is a bug generator. Go's answer is a typed conduit that carries values between goroutines, from a 1978 idea: rather than protecting shared state with locks, pass ownership through a channel so there is no sharing to protect. This lesson builds channels, select, and where the model does not fit.

8 steps·~12 min