Ask an engineer whether their code works and the honest answer is usually "it passes the tests." But a test is a single experiment: you fed the program one input and liked what came out. Pass a thousand such experiments and you have learned that the program is right on those thousand inputs — and precisely nothing about the uncountably many you did not try. The gap between "seems to work" and is correct is not a matter of diligence; it is a matter of kind. Correctness is a claim about all inputs at once, and no finite pile of examples can establish it.
In 1972 Edsger Dijkstra put the point in a sentence that every serious programmer eventually has tattooed on their conscience: "Program testing can be used to show the presence of bugs, but never to show their absence." This page is about what the alternative — a genuine notion of correctness — actually requires. The short version: correctness is not a property a program has on its own. It is a relation between a program and a specification, and you cannot even ask whether code is correct until you have written the specification down.
A specification says what the program is supposed to do, in terms of the
relationship between its inputs and its outputs — deliberately saying nothing about how. The
implementation is the "how." A program is correct exactly when, for every input the
specification admits, the implementation produces an output the specification permits. Correctness is
therefore always relative to a spec: the very same code is "correct" against a weak spec and "buggy"
against a strong one. There is no such thing as absolute, spec-free correctness — "does what I meant" is
not a checkable claim until "what I meant" is written down as
The cleanest form of a specification is a pair: an assumption about the input (the precondition) and a requirement on the output (the postcondition). For an integer square root, for instance:
Notice that the postcondition pins down the answer without hinting at an algorithm — you could satisfy it
by binary search, by Newton's method, or by a lookup table, and all three would be "correct" because
correctness only cares about the input–output relation. Splitting what from how is the
single most important move in the whole subject; we make it formal on the next page,
There are two grades of the claim, and the difference is termination.
The split is not pedantry. Proving the input–output relation and proving termination use completely different machinery — the first is logic about states, the second needs a variant, a quantity that strictly decreases in a well-founded order. So we routinely prove partial correctness first and add termination as a separate step. Keep the distinction sharp: a proof that a loop "computes the right thing" is silent about whether the loop ever ends.
Not every requirement fits the neat input-then-output mould. A program that runs indefinitely — a server, an operating system, a controller — has behaviours that unfold over time, and its correctness is phrased over executions, not just final states. Three families are worth naming now (each gets its own page later in the course).
A useful rule of thumb: safety is what invariants and assertions police; liveness is what variants and
fairness arguments police. We will return to
Here is a routine that returns the index of the midpoint element of a sorted array — the workhorse inside binary search. It "seems to work": test it on arrays of length 3, 7, 100, and every answer is fine.
The defect is invisible to ordinary testing because it only surfaces when binarySearch for
nine years. Now watch what happens the moment you are forced to write the specification:
The postcondition
Because proofs and tests fail in different directions, and you want both. A proof establishes the program matches the specification — but if the specification is itself wrong (you proved the wrong theorem), the proof is impeccable and the program still does the wrong thing. Testing is a cheap, empirical sanity check on the whole stack, spec included: run it, look at reality, catch the mismatch a formal argument silently propagated. Dijkstra's point is not "never test"; it is "testing has a ceiling — it can refute correctness but never confirm it, so do not mistake a green test suite for a guarantee." The mature stance is a layered one: types rule out whole classes of error at compile time, proofs establish the input–output relation, and tests keep the specification and the world honest.
The commonest confusion is to hear "partial correctness" and think it means "correct on part of the inputs" — as in "it works for most cases." It means no such thing. Partial refers to the termination clause being dropped, not to the input space being sampled: a partially correct program is right on every admissible input on which it halts. Conversely, "it passed all 500 tests" is not even partial correctness — it is a statement about 500 inputs, whereas partial correctness quantifies over all of them. Two different words, two different scopes: "partial" weakens the promise about stopping; a test suite weakens the promise about coverage. Never let one stand in for the other.