Loop Invariants and Correctness Proofs

Suppose a colleague hands you a sorting routine and swears it works. The only evidence is that it passed a handful of test cases. Tests can reveal bugs but they can never prove their absence. A loop that mishandles arrays of exactly one element can pass a thousand tests. So can one that silently fails on duplicate keys. Both can still be wrong. What we need is a way to reason about a loop that covers every possible input. This means all inputs, not just the ones we thought to try. This is the role of a loop invariant. It is a precise statement about the state of a computation. We prove it holds before the loop starts. We prove it is preserved by every iteration. Then we exploit it at the end. This lets us conclude the algorithm produced the right answer. Loop invariants matter because almost all non-recursive algorithms are built from loops. A loop is exactly the kind of construct that is easy to get subtly wrong. Examples include an off-by-one in the bound, a variable updated in the wrong order, or an edge case at an empty or singleton input. The invariant technique turns "I ran it and it seemed fine" into an actual mathematical argument. This mirrors how Mathematical Induction turns "the pattern seems to continue" into a proof. It is the standard tool used to certify sorting algorithms, searching algorithms, greedy algorithms, and dynamic programming recurrences. It appears, in disguise, wherever real software has a `for` or `while` loop that a critical system depends on. The big picture is a direct analogy with induction on the natural numbers. Proving a loop correct requires exhibiting a property that is true "at the base case" (before any iterations). It must also be passed along from one iteration to the next (the inductive step). Combined with the fact that the loop eventually stops, this implies the desired postcondition. CLRS packages this as three obligations: initialization, maintenance, and termination. This article works through each one carefully. It includes a fully worked trace on insertion sort. A second, less symmetric example shows the technique is not just a formality for textbook examples. By the end you will be able to state an invariant for a loop you have written. You will check it against the three-part template. You will use it to detect exactly where a buggy loop breaks the argument. In practice, this is usually exactly where the bug lives. This also sets up the natural next step: proving Correctness of Recursive Algorithms by Induction. A loop invariant is nothing more than induction dressed for iteration instead of recursion.

Key takeaways
  • A loop invariant is a property of the program state that holds before and after every iteration of a loop.
  • Correctness proofs via invariants have three parts: initialization, maintenance, and termination.
  • Initialization and maintenance together are an induction argument; termination connects the invariant to the postcondition you actually want.
  • The technique proves correctness for all valid inputs, not just tested cases — it is a proof, not an experiment.
  • A well-chosen invariant should be strong enough to imply correctness at termination, yet provable step by step.
  • The same three-part structure generalizes directly to proving recursive algorithms correct by induction.
The core insight

A loop invariant turns "this loop looks right" into a provable statement by showing it holds initially, is preserved by each iteration, and — together with why the loop must terminate — implies the correct final answer.

The idea

A loop invariant is a logical assertion about the variables of a program — an equation, an inequality, a claim about which elements are sorted, a claim about which set has been visited — that is designed to be true immediately before the loop's test is evaluated, on every pass through the loop, including the very first and the very last. It is not a claim about what the whole algorithm computes; it is a claim about a snapshot of memory at a specific point that recurs over and over as the loop runs. The reason invariants are useful is that a loop is really a compressed, repeated instruction, and repetition is exactly what induction is built to handle. In an inductive proof over the natural numbers, you prove a base case and an inductive step ("if true for kk, then true for k+1k+1"), and conclude the statement holds for all nn. A loop invariant proof does the same thing, indexed not by an abstract integer but by the iteration count of the loop:

  • Initialization. The invariant is true before the first iteration begins. This is the base case.
  • Maintenance. If the invariant is true before an iteration, it remains true before the next iteration. This is the inductive step; here the loop body is analyzed line by line.
  • Termination. When the loop exits — because its guard condition becomes false — the invariant, combined with the reason the loop stopped, implies a useful fact: usually, that the algorithm has done what it claims to do.

Note the asymmetry with plain induction: ordinary induction proves a statement for infinitely many values of nn. A loop invariant argument is used for a finite, bounded process, so the interesting content at termination is not "for all nn" but "for the specific nn at which the loop stopped, look at what the invariant tells us." This is why termination is listed as a separate, distinct obligation, rather than folded into maintenance: you must also argue that the loop terminates at all (usually via a bound that strictly decreases, or an index that strictly increases toward a limit), and then translate the invariant-at-exit into the postcondition you promised the caller. Finally, a subtlety that trips up many first attempts: the invariant must be strong enough to be useful at termination but provable at every step. A trivially true statement (like "the array has a length") satisfies initialization and maintenance vacuously but tells you nothing when the loop ends. Conversely, an overly strong claim (like "the array is fully sorted" stated too early) may be false mid-loop and thus fail maintenance. Choosing the right invariant is the central design skill; this article's job is to show how that choice is made and checked.

How it works

We illustrate the method on insertion sort, chosen because its invariant is the cleanest possible instance of the technique, then apply the same three-part discipline to a second algorithm to show the pattern is general. The algorithm sorts an array A[1..n]A[1..n] in place. On iteration jj, it takes the element A[j]A[j] and inserts it into its correct position among the already-sorted prefix A[1..j−1]A[1..j-1]. ``` INSERTION-SORT(A, n): for j = 2 to n: key = A[j] // Insert A[j] into the sorted sequence A[1..j-1] i = j - 1 while i >= 1 and A[i] > key: A[i + 1] = A[i] i = i - 1 A[i + 1] = key ```

Choosing the invariant. We claim, for the outer `for` loop: > Invariant: At the start of each iteration of the `for` loop (indexed by `j`), the subarray `A[1..j-1]` consists of the elements originally in `A[1..j-1]`, but in sorted order. This is exactly the kind of statement we want: strong enough to be useful (when the loop ends, it will tell us the whole array is sorted), yet checkable at each step.

Initialization. Before the first iteration, j=2j = 2, so the claim is about `A[1..1]`, a single element. A one-element subarray is trivially sorted, and it certainly contains "the elements originally there" since nothing has moved yet. The base case holds.

Maintenance. Assume the invariant holds at the start of iteration jj: `A[1..j-1]` is the original elements of that range, sorted. The loop body removes `A[j]` into `key`, then shifts elements of `A[1..j-1]` that are greater than `key` one position to the right, and finally places `key` into the gap that shifting created. Because `A[1..j-1]` was sorted before this body ran, the shifting process finds exactly the correct insertion point — it stops (`i` decrements) exactly when it meets an element `A[i] <= key` or falls off the front of the array. After insertion, `A[1..j]` contains the same multiset of elements as `A[1..j]` did before (nothing was created or destroyed, only moved and one value re-inserted) and is now sorted. So at the start of iteration j+1j+1, the invariant holds for `A[1..j]` — which is precisely the invariant statement with jj replaced by j+1j+1. The inductive step is verified.

Termination. The `for` loop's counter `j` increases from 2 and the loop exits when `j` exceeds `n`, i.e., after the iteration for j=nj = n completes. Plugging j=n+1j = n+1 into the invariant: `A[1..n]` — the entire array — is the original elements of `A[1..n]` in sorted order. That is exactly the specification of sorting. The proof is complete. Observe how termination did real work here: it is the step where "the invariant holds for `A[1..j-1]` on every iteration" becomes "therefore the entire array is sorted," by noting the specific value of jj at which the guard condition fails.

The inner loop, too

A full proof should also justify the inner `while` loop, since it is where the actual data movement — and the actual risk of an off-by-one bug — lives. > Inner invariant: At the start of each iteration of the `while` loop, the subarray `A[i+1 .. j]` consists of the elements that originally occupied `A[i .. j-1]`, and every element in it is strictly greater than `key`. Initialization: before the loop starts, i=j−1i = j-1, so `A[i+1..j]` is just `A[j..j]`, a single-element (possibly empty in the sense of holding just the shifted-out slot) range; vacuously the claim holds since there is nothing yet to check against `key` (this is the boundary case worth being careful about in an exam-quality proof). Maintenance: each iteration checks `A[i] > key`; if true, it moves `A[i]` one slot right (into `A[i+1]`) and decrements `i`. This preserves the claim: the shifted range grows by exactly the element that was just found to be `> key`. Termination: the loop stops either because `i < 1` (we reached the front of the array) or because `A[i] <= key`. In either case, `A[i+1]` is the correct empty slot for `key` — every element strictly greater than `key` has been pushed one to the right, and either we are at the array boundary or the element now at position `i` is `<= key`. Writing `key` into `A[i+1]` therefore restores full sorted order of `A[1..j]`. This inner-loop termination fact is exactly what the outer maintenance step above relied on. ``` PSEUDOCODE — template for any invariant proof State the invariant I(k), a property indexed by the iteration counter k, true "at the top" of iteration k (before the loop guard is tested). 1. INITIALIZATION: Show I(k0) holds, where k0 is the value of the counter before the first test of the loop guard. 2. MAINTENANCE: Assume I(k) holds and the loop guard is true (so the body runs). Trace the body's effect on the state. Show I(k+1) holds afterward (i.e., before the next guard test). 3. TERMINATION: Identify the value k at which the loop guard first fails. Combine I(k) with "guard is false at k*" to derive the desired postcondition. (Separately: argue the loop guard DOES eventually become false — e.g. a counter is bounded and strictly monotonic — otherwise the algorithm just doesn't terminate and step 3 is vacuous.) ```

Figure
The three obligations of a loop-invariant proof, mapped onto insertion sort's outer loop
j = 2 j = k j = k+1 j = n+1 (exit) INITIALIZATION A[1..1] is a single element: trivially sorted. Base case ✓ MAINTENANCE (iteration k) A[1..k-1] sorted ⟹ insert A[k] via shift-and-place ⟹ A[1..k] sorted. Invariant passed to iteration k+1 ✓ TERMINATION Loop exits at j=n+1: invariant now says A[1..n] is sorted ✓

Complexity & correctness

Strictly speaking, "complexity" here refers to two separate things: the running time of the algorithm being proved correct, and the size/cost of the correctness proof itself. Both are worth being precise about.

Running time of insertion sort. The outer loop runs n−1n-1 times. In iteration jj, the inner `while` loop runs at most j−1j - 1 times (it can only move through the sorted prefix). In the worst case (reverse-sorted input), this gives

∑j=2n(j−1)=n(n−1)2=Θ(n2).\sum_{j=2}^{n} (j-1) = \frac{n(n-1)}{2} = \Theta(n^2).

In the best case (already sorted input), the inner loop's condition `A[i] > key` fails immediately every time, giving Θ(n)\Theta(n). This is unrelated to the invariant proof itself — the proof establishes correctness for all inputs, while the running time bound requires a separate counting argument, exactly the kind covered by Analyzing Loops by Counting Operations. It is worth stressing: an invariant proof says nothing about how fast the algorithm is; a separate operation count is needed for that, and the two analyses are complementary, not substitutes for each other.

Cost of the proof. The invariant technique itself has no "running cost" — it is a static, one-time mathematical argument performed on the algorithm's text, done once regardless of nn. This is one of its main appeals: a single finite proof certifies correctness for every array size and every input ordering simultaneously, which testing can never do (testing all inputs is usually infeasible, and even feasible test suites cannot rule out an input nobody thought to write down).

Correctness argument, restated as induction. Formally, let P(k)P(k) be the invariant statement evaluated with the loop counter equal to kk. The three parts show:

P(k0) (Initialization),P(k)  ⟹  P(k+1) whenever the guard holds (Maintenance).P(k_0) \text{ (Initialization)}, \qquad P(k) \implies P(k+1) \text{ whenever the guard holds (Maintenance)}.

By ordinary mathematical induction, P(k)P(k) holds for every kk the loop counter actually reaches. Since the loop is guaranteed to terminate — insertion sort's outer counter `j` strictly increases and is bounded above by nn, so it must eventually exceed nn — there is a final value k∗k^* at which the guard first fails. P(k∗)P(k^*), plus the negated guard, together imply the postcondition (Termination). This is exactly why the method is airtight: it is induction, and induction is valid for any well-ordered index set, which the iteration counter always is because loops (as opposed to unbounded recursion or non-terminating processes) are only useful algorithms if they halt.

Honesty about tradeoffs. The invariant method proves partial correctness — "if the loop terminates, the output is correct" — plus, separately, termination, together giving total correctness. It says nothing about efficiency, numerical stability, or behavior under concurrent modification of shared state; those require separate arguments (complexity analysis, floating-point error analysis, and race-condition reasoning respectively). It also gives no help in discovering the right invariant — that remains a creative step, usually guided by restating the postcondition and asking "what partial version of this is already true after kk steps?"

Worked example

We trace `INSERTION-SORT` on the concrete array A=[5,2,4,6,1,3]A = [5, 2, 4, 6, 1, 3] (n=6n = 6), checking the outer-loop invariant at the top of every iteration.

jj (start of iteration)Invariant claim: `A[1..j-1]` sorted?Actual contents of `A[1..j-1]`Holds?
2`A[1..1]` sorted`[5]`Yes (trivial)
3`A[1..2]` sorted`[2, 5]`Yes
4`A[1..3]` sorted`[2, 4, 5]`Yes
5`A[1..4]` sorted`[2, 4, 5, 6]`Yes
6`A[1..5]` sorted`[1, 2, 4, 5, 6]`Yes
7 (exit, `j > n`)`A[1..6]` sorted`[1, 2, 3, 4, 5, 6]`Yes — this is the final answer

Reading one row in detail, j=5j = 5 (inserting `key = A[5] = 1`): entering this iteration the invariant guarantees `A[1..4] = [2, 4, 5, 6]` is sorted. The inner loop compares `key = 1` against `6, 5, 4, 2` in turn — all are greater than `1`, so all four shift right, `i` walks down to `0`, and the loop stops because `i < 1`. We write `key` into `A[1]`. The result, `A[1..5] = [1, 2, 4, 5, 6]`, matches the invariant's promise for j=6j = 6: exactly the elements originally in `A[1..5]`, now sorted. Nothing here depended on the specific numbers — the same argument works verbatim for any values, which is the entire point of proving it once rather than testing it repeatedly. At j=7j = 7 the outer guard `j <= n` (i.e. `7 <= 6`) is false, so the loop exits, and the invariant — now instantiated at `A[1..6]`, the whole array — delivers exactly the postcondition "the array is sorted," matching the final row of the table.

A second, less symmetric example: linear search

To show the method is not special to sorting, consider linear search for a target `v` in `A[1..n]`, returning the first matching index or a sentinel: ``` LINEAR-SEARCH(A, n, v): i = 1 while i <= n and A[i] != v: i = i + 1 if i <= n: return i else: return NOT-FOUND ```

Invariant: at the start of each iteration (value `i`), none of `A[1], ..., A[i-1]` equals `v`. Initialization: at `i = 1`, the claim is about the empty range `A[1..0]` — vacuously true. Maintenance: if the loop body runs, the guard was true, meaning in particular `A[i] != v`. Combined with the invariant (`A[1..i-1]` has no match), we get `A[1..i]` has no match — exactly the invariant for the incremented counter `i+1`. Termination: the loop can end two ways. If it ends because `i > n`, the invariant says `A[1..n]` has no match, correctly justifying `NOT-FOUND`. If it ends because `A[i] == v` (while `i <= n`), the invariant says `A[1..i-1]` has no match, so `i` is genuinely the first occurrence — correctly justifying `return i`. Notice the invariant alone is not enough at termination; you must also case-split on which part of the compound guard failed, a detail routinely missed by students proving loops with `and`/`or` conditions.

Common pitfalls & traps

PitfallWhy it happensFix
Invariant too weakStatement is vacuously easy to prove but says nothing at exit (e.g., "the array has n elements")Restate the invariant as a partial version of the actual postcondition
Invariant too strongClaims something only true at the very end, so maintenance fails mid-loopWeaken to what is provably true after exactly kk iterations
Off-by-one in the base indexConfusing "before iteration 1" with "after iteration 1"; array indices starting at 0 vs. 1Fix the exact point ("top of the loop, before the guard test") and stick to it everywhere
Ignoring compound guardsA `while` with `and`/`or` can exit for different reasons; treating exit as one caseCase-split termination on which sub-condition made the guard false
Assuming termination instead of arguing itMaintenance is proven but no argument is given that the loop actually haltsExhibit a strictly monotonic, bounded quantity (a variant function)
Confusing invariant proof with running-time analysisBelieving a correctness proof also says something about speedKeep them separate: use operation counting (see Analyzing Loops by Counting Operations) for time
Silently assuming no aliasing / no concurrent mutationInvariant implicitly assumes the array isn't changed by another thread mid-loopState the assumption explicitly, or prove it holds under the concurrency model in use

Pioneers & history

The loop invariant as a formal proof device grew out of the broader program of program verification: treating a piece of code the way mathematicians treat a theorem, with axioms, preconditions, postconditions, and a calculus for deriving one from another. This effort matured in the late 1960s, converting decades of informal "this loop obviously works" folklore into a rigorous discipline that could, in principle, be checked mechanically.

Origins · 1 minTony Hoare and how this was discovered
TH
Tony Hoare
1934–present · British computer scientist; developed axiomatic semantics and Hoare logic, the formal system in which loop invariants are the central proof rule for iteration, and invented the Quicksort algorithm

Hoare's 1969 paper "An Axiomatic Basis for Computer Programming" introduced what is now called Hoare logic, built around triples {P} S {Q}\{P\} \, S \, \{Q\} meaning "if precondition PP holds and statement SS executes, postcondition QQ holds afterward." The rule for `while` loops in that system is precisely the loop invariant: if {I∧B} S {I}\{I \land B\} \, S \, \{I\} (the invariant II is preserved by the body SS whenever the guard BB holds), then {I}  while B do S  {I∧¬B}\{I\} \; \texttt{while } B \texttt{ do } S \; \{I \land \lnot B\} — the invariant conjoined with the negated guard, exactly what the "termination" step of this article exploits. Around the same period, Robert Floyd's 1967 paper "Assigning Meanings to Programs" had already introduced the closely related idea of associating assertions with points in a flowchart and verifying that execution preserves them — flowchart assertions are the direct ancestor of the loop invariant, and Floyd's method for proving termination via a decreasing well-founded quantity (the "variant") is exactly the missing piece warned about above.

Origins · 1 minhow this was discovered
History
1967
— Robert Floyd publishes flowchart-based assertion methods for proving program properties, including termination via decreasing bound functions.
1969
— Tony Hoare publishes axiomatic semantics ("Hoare logic"), formalizing the loop invariant as the core proof rule for iteration.
1976
— David Gries and others develop systematic, invariant-driven methods for deriving programs alongside their proofs, rather than proving programs after the fact.
1990
s — Loop invariants become a standard pedagogical tool in algorithms textbooks (notably CLRS), used to teach correctness for sorting, searching, and greedy algorithms.
2000
s–present — Invariant-style reasoning underlies mechanized program verifiers and static analysis tools used in safety-critical and security-critical software.

Where this matters

Loop invariants are not merely a classroom exercise; they are the intellectual backbone of how correctness claims about iterative code are actually justified in serious software engineering. Formal verification tools — model checkers, deductive verifiers such as Dafny, and static analyzers built on Hoare logic — require the programmer (or an automated inference engine) to supply loop invariants before they can certify a program; without one, the tool cannot reason past the first iteration of any loop. This matters acutely in safety-critical domains: avionics software, medical device firmware, and railway signaling systems are often required, by certification standards, to carry machine-checked proofs, and every `while` and `for` loop in such code needs an invariant on file. In everyday algorithm design, the invariant is how textbook algorithms — binary search, the partitioning step of Quicksort, Dijkstra's shortest-path relaxation, the greedy exchange arguments behind Huffman coding and minimum spanning trees — are certified correct rather than merely "observed to work on examples." Binary search's invariant ("the target, if present, lies within the current `[lo, hi]` window") is the standard explanation for why the classic implementation bug (`mid = (lo + hi) / 2` overflowing, or an off-by-one in updating `lo`/`hi`) breaks correctness: it is precisely a violation of maintenance. Compiler writers use invariant-style reasoning to justify loop transformations (unrolling, fusion, strength reduction) are semantics-preserving, since an optimization is safe exactly when it does not disturb the invariant the original loop maintained. And database query planners rely on invariant arguments to prove that iterative join and aggregation algorithms produce results consistent with the relational algebra they are meant to implement. Beyond software, the invariant-and-induction pattern is the same reasoning skeleton used to prove correctness of recursive algorithms (each recursive call plays the role of "the next iteration"), which is exactly the natural next topic once loop invariants feel comfortable: Correctness of Recursive Algorithms by Induction. It also connects tightly with Mathematical Induction as the underlying logical principle, and with Analyzing Loops by Counting Operations as the complementary tool needed once correctness, rather than speed, is no longer the open question.

Ask the tutor: Can you show me how to construct a loop invariant for a binary search implementation and use it to explain the classic off-by-one bug?

What exactly counts as "the top of the loop" when stating an invariant?

By convention, the invariant is evaluated at the point immediately before the loop's guard condition is tested — this includes the very first test (before any iteration has run) and every subsequent test right after a body finishes. Being explicit about this point matters because a statement can be true "mid-body" but false at the guard-test instant, or vice versa; sloppy phrasing here is the single most common source of a broken maintenance argument. When writing a proof, always restate the invariant with the loop's counter variable explicitly bound to its value at that instant, rather than describing it loosely in prose.

Is a loop invariant the same thing as an assertion you'd put in code, like `assert(...)`?

They are closely related but not identical. A runtime `assert` checks a condition on one specific execution with one specific input, and only catches a violation if that exact run happens to trigger it. A loop invariant is a proved universal statement, true across all iterations and all valid inputs, established once by mathematical argument rather than checked at runtime. That said, invariants make excellent assertions to insert during debugging: if an assertion encoding your claimed invariant ever fires, you have located a concrete counterexample to your "proof," meaning the proof itself was flawed (or the code doesn't match what you proved).

How do I find a good invariant if none is obvious?

Start from the postcondition — what you want true when the loop ends — and ask what a natural "partial" version of that statement looks like after exactly kk iterations. For a summation loop computing ∑i=1nai\sum_{i=1}^n a_i, the postcondition is "the accumulator equals the sum of all nn terms," so a natural invariant is "the accumulator equals the sum of the first k−1k-1 terms" at the top of iteration kk. This "partial version of the goal" heuristic works for the great majority of straightforward loops; harder cases (e.g. loops with two interacting counters, or in-place algorithms that permute data) usually need an invariant that tracks a relationship between two quantities rather than a single running total.

Does every correct loop have a loop invariant that can be stated this way?

In principle, yes — you can always take the (possibly very complicated) exact characterization of memory at each iteration as "the invariant," so the framework is universal in that trivial sense. In practice, the value of the technique lies in finding a concise, human-checkable invariant, and some loops (especially ones involving complex aliasing, floating-point rounding, or intricate pointer manipulation) require substantially more cleverness to state a tractable one. This is precisely why automated program verifiers spend enormous effort on invariant inference — synthesizing invariants automatically — since supplying one by hand for arbitrary code is often the hardest part of formal verification.

How does a loop invariant proof relate to proving a recursive algorithm correct?

They are the same underlying idea, indexed differently. A loop invariant proof indexes by iteration count and uses ordinary induction; a recursive-algorithm proof indexes by the size of the recursive call (or recursion depth) and typically uses strong induction, with the recursive calls on smaller instances playing the role that "the previous iteration" plays for a loop. The base case of the recursion corresponds to initialization, the argument that a correct recursive call plus correct combining logic yields a correct result corresponds to maintenance, and the fact that recursion depth is bounded (so the base case is eventually reached) corresponds to termination. This parallel is exactly why loop invariants are typically taught immediately before recursive correctness proofs.

Can a loop have more than one invariant, and do I need to prove all of them?

Yes — a single loop can (and complex loops often do) have several invariants tracking different aspects of the state simultaneously, such as one invariant about sortedness and a second about which elements have been "used." You only need enough invariants, taken together, to imply the postcondition you actually care about at termination; extra true-but-unnecessary invariants don't hurt but add proof burden without benefit. In practice it is often cleaner to combine several small facts into one conjunctive invariant statement and carry that single, richer claim through initialization, maintenance, and termination as one unit.

What's the difference between "partial correctness" and "total correctness," and which does a loop invariant proof give?

Partial correctness means: if the algorithm terminates, its output is correct — this is exactly what initialization and maintenance establish. Total correctness additionally requires that the algorithm does terminate, which is the separate obligation handled by the termination step (specifically, by exhibiting a bounded, monotonically changing quantity forcing the guard to eventually fail). A rigorous correctness proof, as presented here, is required to establish both halves; a proof that only covers initialization and maintenance has silently proved partial correctness alone, and that gap is exactly the pitfall highlighted in the warning above.

Before this

Where this leads