Correctness

Week 2, Wednesday

January 14, 2026

Announcements

  • Lab 1 this week: Lists and searching
  • HW1 assigned Friday

The Pillar of Correctness

Trusting Your Code

def ________________(arr, a):
    ret = a
    for i in range(a + 1, len(arr)):
        if arr[i] < arr[ret]:
            ret = i
    return ret
  • What does this function do?
  • Does it work?
  • How do you know?

Testing Is Not Proof

Testing: “It worked on these 10 inputs.”

Proof: “It works on all valid inputs.”

The Key Insight

A loop is just induction in disguise.

Induction Loop Invariant
Base case Initialization
Inductive step Maintenance
“For all \(n\) Termination

The Template

What Is an Invariant?

An invariant is a property that remains true.

“No matter how many times the loop runs, this statement stays true.”

The Template

Step What to show
Invariant State \(P(i)\) for \(i \in \{0, 1, \ldots, n\}\) (your domain)
Base case \(P(0)\) holds (first value in domain)
Inductive step Assuming \(P(j)\) for all \(j\) in domain where \(j < i\), show \(P(i)\)
Termination Plug in \(i = n\) → desired result

Example: Find Min

The Code

def find_min(arr, a):
    """Return index of minimum value in arr[a:]"""
    ret = a
    for i in range(a + 1, len(arr)):
        if arr[i] < arr[ret]:
            ret = i
    return ret
0 1 2 3 4 5 6 7
32 11 73 21 29 86 81 17

If a = 2, what should find_min return?

Step 1: State the Invariant

def find_min(arr, a):
    """Return index of minimum value in arr[a:]"""
    ret = a
    for i in range(a + 1, len(arr)):
        if arr[i] < arr[ret]:
            ret = i
    return ret
0 1 2 3 4 5 6 7 8 9 10 11
32 11 73 21 29 86 81 17 14 3 64 37

Invariant: At iteration \(i\) (for \(i \in \{a+1, \ldots, n\}\)), ret holds the index of the minimum value in arr[a:i].

Step 2: Base Case (\(i = a+1\))

def find_min(arr, a):
    """Return index of minimum value in arr[a:]"""
    ret = a
    for i in range(a + 1, len(arr)):
        if arr[i] < arr[ret]:
            ret = i
    return ret

At the first iteration, i = a+1 and ret = a.

The invariant claims ret is the index of the min in arr[a:a+1].

arr[a:a+1] contains only arr[a], and ret = a is its index. ✓

Step 3: Inductive Step

def find_min(arr, a):
    """Return index of minimum value in arr[a:]"""
    ret = a
    for i in range(a + 1, len(arr)):
        if arr[i] < arr[ret]:
            ret = i
    return ret
0 1 2 3 4 5 6 7 8 9 10 11
32 11 73 21 29 86 81 17 14 3 64 37

IH: Assume the invariant holds at the start of iteration \(i\): ret is the index of min in arr[a:i].

During iteration \(i\), we compare arr[i] to arr[ret]:

  • Case 1: arr[i] < arr[ret] → we set ret = i, the new min
  • Case 2: arr[i] >= arr[ret] → we keep ret, still the min

Either way, after iteration \(i\), ret is the index of min in arr[a:i+1]. ✓

Step 4: Termination

Loop terminates when i = n (after processing the last element).

Plug into invariant: ret is index of min in arr[a:n].

But arr[a:n] is exactly arr[a:].

Therefore: ret is the index of the minimum in arr[a:]. ∎

The Proof in One Slide

Theorem: find_min(arr, a) returns the index of the minimum value in arr[a:].

Proof: By induction on iteration count.

  • Invariant: At iteration \(i\) (for \(i \in \{a+1, \ldots, n\}\)), ret is index of min in arr[a:i]
  • Base case: At \(i = a+1\), ret = a is min of arr[a:a+1]
  • Inductive step: If true at start of iteration \(i\), loop body restores it for \(i+1\)
  • Termination: Plug in \(i = n\)ret is min of arr[a:n] = arr[a:]

Another Example: Sum

The Code

def array_sum(arr):
    total = 0
    for i in range(len(arr)):
        total += arr[i]
    return total

What’s the invariant?

The Invariant

Invariant: At iteration \(i\) (for \(i \in \{0, 1, \ldots, n\}\)), total equals the sum of arr[0:i].

The Proof (Quick Version)

  1. Base case: At \(i = 0\), total = 0 = sum of arr[0:0]. ✓

  2. Inductive step: Assume invariant holds at start of iteration \(i\): total = sum of arr[0:i].

    During iteration \(i\), we add arr[i] to total.

    After iteration \(i\): total = sum of arr[0:i] + arr[i] = sum of arr[0:i+1]. ✓

  3. Termination: Plug in \(i = n\)total = sum of arr[0:n] = sum of all. ∎

Composing Proofs

Selection Sort

def selection_sort(arr):
    for a in range(len(arr)):
        min_idx = find_min(arr, a)
        arr[a], arr[min_idx] = arr[min_idx], arr[a]
  • This uses our find_min helper!

  • Quick check: running time?

The Decomposition Principle

We already proved find_min(arr, a) returns the index of the minimum in arr[a:].

Now we can use that fact without re-proving it.

Prove small pieces. Compose them into bigger proofs.

The Selection Sort Invariant

Invariant: At iteration \(a\) (for \(a \in \{0, 1, \ldots, n\}\)), arr[0:a] is sorted and contains the \(a\) smallest elements.

The Proof

  1. Base case: At \(a = 0\), arr[0:0] is empty. ✓ (Trivially sorted, trivially smallest.)

  2. Inductive step: Assume invariant holds at start of iteration \(a\): arr[0:a] is sorted and contains the \(a\) smallest elements.

    During iteration \(a\):

    • find_min(arr, a) returns the index of the minimum in arr[a:] (by our earlier proof!)
    • After the swap, arr[a] holds the \((a+1)\)-th smallest element

    After iteration \(a\): arr[0:a+1] is sorted and contains the \(a+1\) smallest elements. ✓

  3. Termination: Plug in \(a = n\)arr[0:n] is sorted. ∎

The Lesson

Decomposition is good system engineering.

Software Proofs Data Pipelines
Write small, tested functions Prove small helper lemmas Build validated transformations
Compose into larger programs Compose into larger proofs Chain into workflows
Reuse without re-implementing Reuse without re-proving Reuse without re-validating

Finding the Invariant

The Art

Stating the invariant is often the hardest part.

Strategy: Ask “What property do I maintain that leads to my answer?”

Common Patterns

Algorithm Type Invariant Pattern
Accumulation var holds result for elements seen so far”
Search “If target exists, it’s in the remaining range”
Partition “Elements before \(i\) satisfy property P”
Two-pointer “Answer is not in the excluded region”

Design, Not Just Proof

Loop invariants aren’t just for proving code correct.

They’re a design tool.

“What should be true at each step?”

If you can’t state the invariant, you might not understand your own algorithm.

A Warning

A wrong invariant can “prove” incorrect code correct.

The invariant must be:

  1. True (actually holds at each iteration)
  2. Strong enough (implies correctness at termination)
  3. Preserved (maintained by the loop body)

A Bad Example

Weak invariant: “At iteration \(a\), arr[0:a] is sorted.”

0 1 2 3 4 5 6 7 8 9 10 11
12 25 38 47 59 83 71 6 94 52 15 33

At iteration \(a = 5\), suppose we have:

0 1 2 3 4 5 6 7 8 9 10 11
12 25 38 47 59 83 71 6 94 52 15 33

Is arr[0:5] = [12, 25, 38, 47, 59] sorted? Yes!

But is this correct? No! The element 6 is smaller than all of them.

Why This Matters

For Your Career

Real bugs in real systems come from:

  • Off-by-one errors
  • Edge cases not handled
  • “It usually works” code

Loop invariants catch these before they ship.

The Mindset

Every time you write a loop, ask:

  1. What’s true at the start?
  2. What stays true each iteration?
  3. What does that give me at the end?

Summary

The Loop Invariant Template

Step What to show
Invariant State \(P(i)\) for \(i \in \{a, a+1, \ldots, n\}\) (your domain)
Base case \(P(a)\) holds (first value in domain)
Inductive step Assuming \(P(j)\) for all \(a \leq j < i\), show \(P(i)\)
Termination Plug in \(i = n\) → desired result

This is induction on the iteration count.

Looking Ahead

Wednesday: We’ll use loop invariants to prove a famous algorithm correct — and see why it took 17 years to get a bug-free version.

Practice Problems

Before Wednesday, try stating invariants for:

  1. reverse(arr) — reverse an array in place

  2. is_sorted(arr) — check if array is sorted

  3. count(arr, target) — count occurrences of target

Think: What’s true after processing \(i\) elements?