01

Weakest Preconditions

One simple question, asked about your code:
“What must be true before this runs, so that what I want is true after?”

Plain idea

Figure out the exact input condition that makes code do the right thing

The trick

Read the code backwards, from the goal to the start

Why bother

Catch bugs by thinking, not just by testing & hoping

Lots of concrete code and real numbers ahead — no heavy math needed. (Idea: Dijkstra; teaching style: Gries.)

Start with a concrete question

Which starting states let a program finish with the required result?

Your goal

Understand the data, the rule, and the required result. Then run the complete Python program and modify one thing at a time.

No companion presentation is required. Python syntax and any mathematical notation are explained as they are introduced.

Vocabulary you need before the algorithm

State

A mapping from variable names to current values.

Assignment

Evaluate the right side, then update a name.

Predicate

A true/false statement about a state.

Weakest precondition

The most permissive condition guaranteeing normal termination and the goal.

Work one example before writing code

Subtract 100, require balance≥0: balance−100≥0 means balance≥100. Starting at 99 gives −1; starting at 100 gives 0.

Trace each step and name what changed. Ask which fact stayed true and what should happen on an empty, invalid, or boundary input.

02 / Why

“Will this actually work?” THE EVERYDAY PROBLEM

Here's a tiny function. When is it safe to call? Testing a few inputs gives you some confidence. We want certainty.

A withdrawal

Teaching excerpt · complete runnable lab available from “Code”
def withdraw(balance):
    balance = balance - 100
    return balance

# We want one thing to always hold:
# the balance must never go negative.
#
# So AFTER running:  balance >= 0

Testing only samples

  • Try balance = 500 → ends at 400. Fine.
  • Try balance = 100 → ends at 0. Fine.
  • Try balance = 50 → ends at −50. Bug!

Tests found one bad case by luck. What we really want is the exact rule for when it's safe — for every input, not just the ones we tried.

That exact rule is the weakest precondition.

03 / Vocabulary

Two Words: Before & After THE CONTRACT

Every piece of code has an implied contract: “if X is true when you call me, then Y will be true when I'm done.”

Precondition — the “before”

What must be true when the code starts. The caller's responsibility.

e.g. balance >= 100

Postcondition — the “after”

What you want to be true when the code finishes. The goal.

e.g. balance >= 0

Written as a contract

Teaching excerpt · complete runnable lab available from “Code”
# { before:  balance >= 100 }
balance = balance - 100
# { after:   balance >= 0   }

Read it as a promise:

If balance >= 100 going in, then balance >= 0 coming out — guaranteed.

The curly-brace notation {before} code {after} is the standard way to write this. That's all it means.

04 / The idea

What wp Actually Means ONE QUESTION

wp(code, goal) answers:

“What's the condition on the input that guarantees the goal afterward?”

wp is short for weakest precondition. Give it two things:

  • the code that will run,
  • the goal you want true afterward,

and it hands back the before-condition you need.

Picture it as a machine

wp( code , goal ) work it out backwards goal (after) before-condition

“Guarantees” also means the code actually finishes — no infinite loops. We'll see how loops earn that on slides 15 to 20.

05 / The idea

Why “Weakest”? = MOST PERMISSIVE

Many before-conditions would be “safe.” We want the one that rules out the fewest inputs — the most generous rule that still works.

For withdraw, goal balance >= 0

Before-conditionSafe?Verdict
balance == 100yestoo strict — forbids 500
balance >= 1000yesway too strict
balance >= 100yesjust right — weakest
balance >= 50notoo loose — 50 breaks it

Weakest = the exact boundary

50 100 500 safe: balance >= 100 unsafe

The weakest precondition is the line in the sand: everything on the safe side works, and it doesn't exclude a single input that would have been fine. That makes it the most reusable rule.

06 / The trick

The Whole Trick: Read It Backwards GOAL → START

Start from the goal (the “after”) and push it back through each line until you reach the start. Let's do our withdrawal, with real numbers.

Push the goal through the line

Teaching excerpt · complete runnable lab available from “Code”
# goal (after):  balance >= 0

# the line that runs:
balance = balance - 100

# Ask: for the RESULT to be >= 0,
# what must balance be BEFORE?
# The result is (balance - 100), so:
       balance - 100 >= 0
       balance >= 100   # answer!

Check it with numbers

beforeafter −100>= 0 ?
1000yes
15050yes
99−1no

balance >= 100 is exactly the cut-off. We didn't guess-and-test — we computed it by substituting the new value back into the goal.

Notice we replaced balance in the goal with what it becomes: balance - 100. That's the one rule for assignments →

07 / Rule 1

Rule for Assignment: Copy & Replace x = e

To find the before-condition of x = e:
take the goal, and replace every x with e.

Example A

Teaching excerpt · complete runnable lab available from “Code”
# goal: x > 5
x = x + 1
# replace x with x+1
→ x + 1 > 5
→ x > 4

To end above 5 after adding 1, start above 4.

Example B

Teaching excerpt · complete runnable lab available from “Code”
# goal: y == 10
y = y * 2
# replace y with y*2
→ y * 2 == 10
→ y == 5

To land on 10 after doubling, start at 5.

Example C

Teaching excerpt · complete runnable lab available from “Code”
# goal: total <= 50
total = total + item
→ total+item <= 50
→ total <= 50 - item

Room left before adding = 50 minus the item.

Common trap: people substitute into the before picture (“x becomes 5, so…”). Don't. You always substitute into the goal (the after), because that's the thing you're trying to make true.

08 / Rule 2

Several Lines: Go Bottom to Top ONE LINE AT A TIME

For a sequence of statements, push the goal up through the last line first, then the line above, and so on.

The program

Teaching excerpt · complete runnable lab available from “Code”
x = x + 1
y = x * 2
# goal (after): y > 10

Work upward

Teaching excerpt · complete runnable lab available from “Code”
Step 1 — through y = x*2
   goal y > 10, replace y with x*2
   → x*2 > 10
   → x > 5

Step 2 — through x = x+1
   need x > 5, replace x with x+1
   → x+1 > 5
   → x > 4   # final answer

Sanity check with numbers

start xx+1y = 2×y>10?
5612yes
4510no
101122yes

x > 4 is exactly right — x = 4 just misses (y lands on 10, not above it), x = 5 just makes it.

Rule in one line: wp(A; B, goal) = wp(A, wp(B, goal)). Do the inner one (B) first.

09 / Rule 3

if / else: Both Branches Must Work CHECK EACH PATH

For an if, the before-condition is: whichever branch runs, it must reach the goal. So check every path and require them all.

Absolute value

Teaching excerpt · complete runnable lab available from “Code”
if x >= 0:
    r = x
else:
    r = -x
# goal: r == abs(x)  (r is |x|)

Check each path:

Teaching excerpt · complete runnable lab available from “Code”
# when x >= 0, we run r = x
#   is x == |x|?  yes ✓ (x is non-neg)

# when x < 0, we run r = -x
#   is -x == |x|? yes ✓ (x is neg)

Result

Both branches reach the goal, so the before-condition is true — it works for every x. No precondition needed.

The rule

Teaching excerpt · complete runnable lab available from “Code”
wp(if B: S1 else: S2, goal)
  =  (B      and wp(S1, goal))
     or
     (not B  and wp(S2, goal))

In words: either the test is true and the if-branch reaches the goal, or the test is false and the else-branch does. Miss a case (e.g. no else) and you must prove the goal already holds there.

10 / Worked example

Putting It All Together ONE FULL DERIVATION

An if, then an assignment. We use all three rules, one step at a time, and never guess. All numbers are integers.

The program

Teaching excerpt · complete runnable lab available from “Code”
if a > b:
    m = a
else:
    m = b
m = m + 1
# goal: m > a and m > b

In words: “m ends up bigger than both.” When is that guaranteed?

Answer

The before-condition is true. It works for every a and b, and the derivation proves it.

Work upward

Teaching excerpt · complete runnable lab available from “Code”
Step 1 — through m = m + 1 (Rule 1)
   replace m with m+1 in the goal
   → m+1 > a and m+1 > b
   → m >= a and m >= b     # integers

Step 2 — through the if (Rule 3)
   branch a > b runs m = a:
     a >= a and a >= b   → true (a > b)
   branch a <= b runs m = b:
     b >= a and b >= b   → true (a <= b)

   (a>b and true) or (a<=b and true)
   → true   # final answer

Step 1 used a small integer fact: m + 1 > a is the same as m ≥ a. With real numbers you would keep m > a - 1 instead.

11 / Watch out

Two Traps: No else, and Code That Crashes EASY TO MISS

Python supports negative indices from -len(a). Use 0≤i only when it is part of your API's nonnegative-index convention.

Trap 1 · An if with no else

A missing else is really else: pass. That path changes nothing, so the goal must already be true there.

Teaching excerpt · complete runnable lab available from “Code”
if x > 10:
    x = x - 10
# goal: x <= 10

# test true : x > 10  and x-10 <= 10  → 10 < x <= 20
# test false: x <= 10 and x <= 10     → x <= 10
# together  : x <= 20

So x = 25 breaks it: it becomes 15. One subtraction is not enough. A test with x = 12 would never show you that.

Trap 2 · The expression must be defined

Code that crashes never reaches the goal. So wp quietly adds “this line can run at all.”

Teaching excerpt · complete runnable lab available from “Code”
y = 10 / x
# goal: y > 2

# replace y:   10 / x > 2
# plus defined: x != 0
# solve:       0 < x < 5
x10 / x> 2 ?
42.5yes
52.0no
0crashnever gets there

Python list indexing is defined for -len(a) ≤ i < len(a). If an API promises only nonnegative indices, add 0 ≤ i as a separate contract condition. Definedness alone does not forbid Python's negative indices.

12 / Loops

Loops Are the Hard Part — Here's the Idea DON'T UNROLL

Invariants yield sufficient conditions; the exact total-correctness weakest precondition has a least-fixed-point characterization and is not computable for arbitrary programs.

An arbitrary loop needs a fixed-point or inductive argument rather than one assignment-style substitution. Invariants plus a well-founded variant prove sufficient input conditions; they need not produce the exact weakest precondition.

1 · The invariant

A fact that stays true every time around the loop — before it starts and after each pass.

Think: “what's my progress-so-far statement?” It captures the work done up to now.

This is the creative part — and the single most useful idea for real code.

2 · The bound (why it stops)

A nonnegative integer variant strictly decreases while the loop runs. The loop may stop before the variant reaches zero.

If something keeps getting smaller and can't go below zero, the loop must end. That's your termination proof.

Usually “how far left to go” — like n - i.

Slogan: the invariant says the loop is doing the right thing; the bound says it won't do it forever.

13 / Loops

Finding the Invariant — a Real Loop SUM 1..n

Add up 1 + 2 + … + n. Watch what stays true after every pass — that's the invariant.

Teaching excerpt · complete runnable lab available from “Code”
i = 0
s = 0
while i != n:
    i = i + 1
    s = s + i
# goal: s == 1+2+...+n

Trace it for n = 4 and watch s vs 1..i:

after passis1+..+i
start000
1111
2233
3366
441010

The invariant jumps out

s == 1 + 2 + … + i  and  0 ≤ i ≤ n

“s always holds the sum up to i so far.” True at the start (0 = empty sum) and true after every single pass — look at the table, the last two columns always match.

The second part, 0 ≤ i ≤ n, looks boring but matters: it needs the precondition n ≥ 0. Call it with n = -1 and i != n is true forever.

Why it gives the answer

The loop stops when i == n. Plug that into the invariant:

s == 1 + 2 + … + n  ✓

invariant (still true) + loop-finished (i == n) = the goal. That's the whole point of picking a good invariant.

14 / Loops

Proving It Stops — the Bound A COUNTDOWN

Pick something that shrinks

For the sum loop, how much work is left? n - i. Watch it fall for n = 4:

ibound = n − i
04
13
22
31
40 → loop stops

Two things to confirm

  • While looping (i != n), the bound is > 0. ✓ (the invariant says i ≤ n, and i != n, so i < n)
  • Each pass makes it strictly smaller (i goes up by 1, so n - i drops by 1). ✓

A whole number that keeps dropping and never goes below 0 cannot drop forever — so the loop must end. Termination proven, no guesswork.

If you can't find such a countdown, that's a red flag your loop might hang.

15 / Loops

The Loop Checklist (All Together) 4 QUICK CHECKS

Once you have an invariant P and a bound t, a correct loop is just these four checks. Here they are for the sum loop.

CheckIn plain wordsSum loop
1 · Starts trueThe setup makes the invariant hold before the first pass.i=0, s=0 → s = empty sum = 0 ✓
2 · Stays trueIf it's true and we loop once more, it's still true.add i+1 to both i and s → still “s = 1..i” ✓
3 · Gives the goalInvariant + loop-finished ⇒ the postcondition.s=1..i and i=n → s=1..n ✓
4 · It endsThe bound is > 0 while looping and shrinks each pass.n-i > 0, drops by 1 each time ✓

Invariant used here: s == 1+..+i and 0 ≤ i ≤ n, with precondition n ≥ 0. Gries' book splits check 4 in two (“bound > 0 while looping” and “bound drops each pass”), so you will also see this called the five checks.

All four tick → the loop is correct and terminates, for every valid input. Checks 1–3 are the invariant's job; check 4 is the bound's job.

16 / Loops

Loop Example 2: Linear Search FIND THE FIRST x

Look through list b (length n) for the first place that holds x. Same four checks, new loop.

Teaching excerpt · complete runnable lab available from “Code”
i = 0
while i < n and b[i] != x:
    i = i + 1
# goal: i == n  and x is not in b
#   or  i < n   and b[i] is the FIRST x

Invariant: 0 ≤ i ≤ n and x is not in b[0..i-1] (“everything I passed was not x”).
Bound: n - i.

Trace b = [4, 8, 15, 16], x = 15:

ib[i]passed so farbound
04(none)4
1843
215 → stop4, 82

The four checks

1 Starts true — i = 0: nothing passed yet, and “x is not in an empty list” is true. ✓

2 Stays true — we only step past b[i] when b[i] != x, so “not in b[0..i]” still holds. And i < n, so i+1 ≤ n. ✓

3 Gives the goal — the loop stops for one of two reasons. i == n: x is in none of b. b[i] == x: found, and it is the first, because none before it matched. ✓

4 It ends — the guard says i < n, so n - i > 0; each pass drops it by 1. ✓

Order matters: i < n and b[i] != x checks the index first. Python's and stops early, so b[n] is never read. Swap the two tests and you get an IndexError when x is missing.

17 / Loops

Loop Example 3: Binary Search HALVE EACH PASS

The list a is sorted. Find the leftmost spot where x could go. This loop is famous for off-by-one bugs; the invariant removes them.

Teaching excerpt · complete runnable lab available from “Code”
lo, hi = 0, len(a)
while lo < hi:
    mid = (lo + hi) // 2
    if a[mid] < x:
        lo = mid + 1
    else:
        hi = mid
# goal: everything in a[:lo] < x
#       everything in a[lo:] >= x

Picture of the invariant:

Teaching excerpt · complete runnable lab available from “Code”
 0          lo           hi          n
 |  all < x  |  not seen  |  all >= x |

Bound: hi - lo, the size of the “not seen” part.

Trace: a = [2, 5, 5, 9, 12], x = 5

lohimida[mid]movebound
0525hi = 25
0215hi = 12
0102lo = 11
11stop: answer 10

The four checks

1 Start: both outer parts are empty, so true. ✓

2 a[mid] < x means mid and all left of it are < x (sorted), so lo = mid + 1 is safe. Otherwise mid and all right of it are ≥ x, so hi = mid. ✓

3 Stop means lo == hi: the middle part is empty, and that is the goal. ✓

4 lo ≤ mid < hi, so both moves shrink hi - lo by at least 1. ✓

Write hi = mid - 1 and check 2 fails: mid might be the answer, and you just threw it away.

18 / Good to know

Three Sanity Facts About wp QUICK INTUITION

You don't need these to use the method, but they explain why it behaves so predictably.

No miracles

wp(code, false) = false

No input can make an impossible goal come true. Code can't perform magic.

Easier goal, easier input

If goal A is easier than goal B, then A needs a looser before-condition.

Asking for less never demands more of the caller.

Split & conquer

wp(S, A and B)
= wp(S, A) and wp(S, B)

To hit two goals at once, hit each — then combine. Handy for proving compound goals piece by piece.

These are just consistency guarantees. If some rule you wrote ever broke one of them, the rule would be wrong — they're the guardrails that keep the whole calculus honest.

19 / In practice

You Already Half-Do This EVERYDAY PAYOFF

You don't have to write formal proofs to benefit. The habits show up directly in code you write today.

Preconditions → guards & asserts

Teaching excerpt · complete runnable lab available from “Code”
def withdraw(balance, amt):
    assert balance >= amt   # the wp!
    return balance - amt
# the assert IS the weakest
# precondition, written down.

Invariants → fewer off-by-ones

Just naming “what's true each pass” catches most loop bugs — boundaries, empty inputs, the last element — before you ever run the code.

Backward reasoning → debugging

“This line needs x > 0… so the line above must guarantee it…” is exactly wp, done by hand. It's how you localise a bug fast.

It's the engine of real tools

Verifiers like Dafny, Why3, and ESC/Java compute weakest preconditions under the hood to check code automatically. Same idea, scaled up.

20 / Mistakes

Common Mistakes (and the Quick Fix) STUDENT TRAPS

Almost every wrong answer on a wp exercise is one of these six.

MistakeWhy it is wrongFix
Working top-downThe goal lives at the end. Going forward you have nothing to substitute into.Start at the last line and move up.
Substituting the wrong wayFor x = x + 1 with goal x > 5, writing x - 1 > 5 runs the line backwards. wp is not “undo.”Replace x with the right-hand side, exactly: x + 1 > 5.
Forgetting the silent pathAn if with no else still has a second path, where nothing happens.Add (not B and goal) for the missing branch.
Account for expression-definedness. Division needs a nonzero divisor; Python indices may be negative down to -len(a), unless the API specifically excludes them.a[i] and y / x need 0 ≤ i < len(a) and x != 0.Add the “defined” part to every step.
An invariant that is too weaki ≥ 0 is true every pass, but check 3 fails: it says nothing about s.Ask: “at the end, does invariant + stop give the goal?”
A bound that can stallIf one branch of the body does not change the bound, the loop can spin forever.Check every branch shrinks it by at least 1.

Self-check habit: after you compute a wp, test it on one value just inside the line and one just outside. If x > 4 is your answer, try x = 5 and x = 4.

21 / Practice

Check Yourself TRY FIRST, THEN OPEN

Work each one on paper. Then click “Answer” to compare. All variables are integers.

1 · Assignment

wp(x = x - 3, x ≥ 0)

Answer

Replace x with x - 3: x - 3 ≥ 0, so x ≥ 3.

2 · Two lines

x = 2*x; y = x + 1, goal y > 7

Answer

Through y = x+1: x > 6. Through x = 2*x: 2x > 6, so x > 3. Try 4: 8, 9 ✓. Try 3: 6, 7 ✗.

3 · No else

if x < 0: x = -x, goal x > 0

Answer

(x < 0 and -x > 0) or (x ≥ 0 and x > 0) = x < 0 or x > 0 = x != 0. Zero stays zero.

4 · Defined?

y = 12 // x, goal y == 3

Answer

Need x != 0 and 12 // x == 3. Only x == 4 works (12 // 3 is 4, 12 // 5 is 2).

5 · Find the invariant

Teaching excerpt · complete runnable lab available from “Code”
i, p = 0, 1
while i != n:
    p = p * 2; i = i + 1
# goal: p == 2**n
Answer

Invariant p == 2**i and 0 ≤ i ≤ n. Bound n - i. Needs n ≥ 0.

6 · Spot the bug

Teaching excerpt · complete runnable lab available from “Code”
i = 0
while i < n:
    if b[i] == 0: continue
    i = i + 1
Answer

Check 4 fails. When b[i] == 0 the bound n - i does not drop, so the loop never ends.

22 / Glossary

Glossary EVERY WORD IN ONE PLACE

PreconditionWhat must be true when code starts. The caller's job.
PostconditionWhat will be true when code ends. The goal.
Hoare triple{P} S {Q}: if P holds before S, then Q holds after.
wp(S, Q)The weakest precondition: the most generous P that makes {P} S {Q} true.
Stronger / weakerA is stronger than B if A ⇒ B. Stronger = fewer states allowed.
Implies (⇒)A ⇒ B: whenever A is true, B is too. {P} S {Q} holds exactly when P ⇒ wp(S, Q).
GuardThe test of an if or while, like i != n.
Loop invariantA fact true before the loop and after every pass. Your “progress so far.”
Bound functionA whole number, > 0 while looping, that drops every pass. Proves the loop ends.
Partial correctness“If it ends, the answer is right.” Checks 1–3.
Total correctnessPartial correctness plus it always ends. All 4 checks. wp means this one.
DefinedAn expression can be computed: no divide by zero, no bad index.
23 / Recap

The Cheat Sheet ONE PAGE

The rules

CodeBefore-condition
x = egoal with x replaced by e
A ; Bdo B first, then A (bottom–up)
if / elseeach branch must reach the goal
no elsethe silent path must already meet the goal
a[i], y / xadd “defined”: index in range, divisor ≠ 0
whileinvariant + bound (4 checks)

The loop checks

1 starts true · 2 stays true · 3 gives the goal · 4 it ends.

Remember three things

  • Read backwards — from the goal to the start.
  • Weakest = most generous input rule that still works.
  • Loops = invariant + bound — “doing right” + “won't run forever.”

Run a complete program

Which starting states let a program finish with the required result?

The file contains its own imports, data generator or fixture, implementation, and checks.

python3 -m venv .venv
source .venv/bin/activate
# No third-party packages required
python weakest_preconditions.py
python weakest_preconditions.py --self-test

Use Python 3.10+. On Windows, activate with .venv\Scripts\Activate.ps1. Required packages: Python standard library.

Connect the concept to the implementation

Read the contract first. Identify the inputs, intermediate state, and return value in this excerpt from the verified file.

def wp_at(transform, goal, state):
    """Pre: transform terminates. Post: True iff it completes normally and meets goal."""
    try:
        return bool(goal(transform(dict(state))))
    except (ZeroDivisionError, IndexError, KeyError):
        return False

Compare with an actual run

Safe starting balances in 95..105: [100, 101, 102, 103, 104, 105]
12//x==3, safe x in -5..8: [4]
Python index -1 is defined: True

Output was captured from the complete program running in an isolated temporary directory. Floating-point digits and compressed sizes may vary across versions/hardware.

Check the result, then try an exercise

Verified checks

533 checks passed, including boundary cases and independent reference comparisons where applicable. These executed checks complement the reasoning; they are not a universal proof.

Derive the precondition for x=2*x; y=x+1 with goal y>7. Then verify it on integers −10 through 10.

Explain the expected result

Substitute through y first: x>6. Then through x: 2x>6, so original x>3. This finite test supports the derivation; it is not the general proof.

Implement it yourself and validate it

  1. Run the downloadable program and its checks.
  2. Reimplement wp_at from its contract.
  3. Compare outputs with the reference on valid, empty, and invalid cases.
  4. Explain the limitations of the algorithm or experiment.
All coding labs
Decks