LGTM Is Not a Theorem

Pragmatic Verification And The Economics Of Software Confidence
Durwasa Chakraborty (he/him)
PhD :: Formal Verification & Programming Languages, IIT Madras
MS :: Northeastern University, Boston MA
BE :: Government Engineering College, Jabalpur MP
INDIAFOSS 2026

Refunds. Tuesday.

AUTHORIZEDfunds held on the card
→
CAPTUREDfunds taken from them
→
SETTLEDmoney in our account

capture → settlement is T+1. Today we only refund what has settled.

coverage report for refund_v1.py: 100%, 5 statements, 5 run, 0 missing, 0 partial
2 passed 100% 0 missing 0 partial

Wednesday: the same-day cancellation

Settlement lags capture by a day, so a customer who cancels that afternoon is CAPTURED, not SETTLED, and support cannot refund them.

    if charge.status is Status.SETTLED:
        gw.refund(charge.amount_minor)
        charge.refunded = True
    elif charge.status is Status.CAPTURED:      # the ticket
        gw.refund(charge.amount_minor)
        charge.refunded = True
75%  ·  missing 9-10 copy the test, change one word  →  100%

Copy the test. Change one word.

the test that already exists
def test_refunds_a_settled_charge():
    c = Charge(1, Status.SETTLED, 24900)
    gw = Gateway()
    refund(c, gw)
    assert gw.refunds == [24900]
the one you paste under it
def test_refunds_a_captured_charge():
    c = Charge(3, Status.CAPTURED, 24900)
    gw = Gateway()
    refund(c, gw)
    assert gw.refunds == [24900]
3 passed 100% 0 missing 0 partial

Nine seconds of work. The build is green. Ship it.

Thursday, 02:14. Sev-2.

The gateway call for a refund times out. The job retries it. Nothing in refund() remembers that the first one already went through.

[SEV-2] refunds: duplicate payouts in prod
refund(charge 3)  gw.refund(24900)  ok  timeout
refund(charge 3)  gw.refund(24900)  ok  ← retry
gw.refunds: [24900, 24900]  refunded twice
49800 out on a 24900 charge every retried refund pays twice coverage: still 100%

We are losing money. Patch it tonight.

So you patch it

the guard
def refund(charge, gw) -> None:
    if charge.refunded:      # the patch
        return
    if charge.status in REFUNDABLE:
        gw.refund(charge.amount_minor)
        charge.refunded = True
and the test for it  82% → 100%
def test_refunding_twice_refunds_once():
    c = Charge(4, Status.SETTLED, 24900)
    gw = Gateway()
    refund(c, gw)
    refund(c, gw)
    assert gw.refunds == [24900]
4 passed 100% 0 missing 0 partial
gw.refunds: [24900]  retry is a no-op

Idempotent now. Incident closed. Properly fixed.

More testing… more testing… more testing…

AIB sketch still, subtitle: Testing mein bitha denge

Testing will not catch this one.

Two ways in. The same broken state.

one worker, a timeout
whostep(r, p)
Aread flag → false(false, 0)
Agw.refund()(false, 1)
A504, exceptionraises
Arefunded = Truenever runs
retryread flag → false(false, 1)
retrygw.refund()(false, 2)
two workers, no failure at all
whostep(r, p)
Aread flag → false(false, 0)
Bread flag → false(false, 0)
Agw.refund()(false, 1)
Bgw.refund()(false, 2)
Arefunded = True(true, 2)
Brefunded = True(true, 2)

Both pass through (false, 1), money gone, flag still down. The guard reads the flag, so it waves both of them through.

Then stop guessing the schedule

A test has to pick an interleaving, and there are exponentially many. So pick none. Instead prove that every single step keeps one sentence true.

Agree(r, p)  ≡  p ≤ 1  ∧  ( r = false ⇒ p = 0 ) the flag and the money agree

If it holds at the start, and no step can break it, it holds in every reachable state, under every interleaving, including the ones you never thought of.

A trace you found. An obligation you cannot discharge.

assume  Agree(false, 0)
step    t₂ : p += 1   (gw.refund returns)
show    Agree(false, 1)

  1 ≤ 1  ✓
  r = false ⇒ p = 0   becomes   1 = 0   ✗ unprovable

The goal itself hands you the bug, and every other route to it you never imagined.

The same step, written twice

what is proved
def cas (s : St) : St :=
  if s.refunded then s
  else ⟨true, s.payouts + 1⟩

theorem cas_preserves :
  Agree s → Agree (cas s)

theorem cas_safe (n : Nat) :
  Safe (run n ⟨false, 0⟩)
what ships
claimed = (Charge.objects
    .filter(id=cid, refunded=False)
    .update(refunded=True))

if claimed == 0:
    raise AlreadyRefunded

gw.refund(charge.amount_minor)

One SQL statement is one atomic step, so the model has one too. Read the flag and write it in the same move, and (false, 1) stops being reachable, for any number of workers, in any order.

So why don’t we prove everything?

If the mathematics is this good, why are any of us still writing tests?

Well… because…

software has economics you spend something, you get something, and it has to pay for itself

If it goes through, it is a proof

And if the proof goes through on your machine, it is a proof. Not probably. Not mostly. The checker does not care who wrote it, or whether they understood it.

That is not true of generated code.

it proves the statement you gave it whether that statement says the right thing is still yours

The curve moved

The steep part used to be producing the proof. That is what took years to learn, and it is the part you now hand over.

What is left is different.

coming up with the theorem
reading the statement
deciding whether it is enough

The skill is not writing the proof. *

* at scale, proof size and token cost matter again. This is about the first step.

You have shipped this a hundred times

Refunds
rfnd_8821acc_42249.00settled
rfnd_8822acc_17899.00settled
rfnd_8823acc_91120.00settled
rfnd_8824acc_081,499.00settled
rfnd_8825acc_6375.00settled
12 34 5› 23 refunds · 5 per page

Twenty-three rows, five to a page. Something has to decide which rows land on which page, and that something is a handful of lines in Django.

A very small chunk of Django

The code that decides which rows land on which page, and the suite that covers it.

coverage report for the paginator slice at 100 percent, 11 statements, 11 run, 0 missing, 0 partial

It is arithmetic. What could go wrong?

Fair. I mutated those lines nine ways: off-by-ones, flipped comparisons, a dropped orphans. Every one was caught. The suite is good.

Then I read the rest of the file:

if self.per_page <= self.orphans:
    # RemovedInDjango70Warning: ... replace with:
    # raise ValueError(
    #     "The orphans argument cannot be larger than or "
    #     "equal to the per_page argument."
    # )

Django shipped that for years before anyone wrote the precondition down.

One line, and what it buys

before Django 7.0
Paginator(rows, per_page=2, orphans=20)

  22 rows → 1 page of 22
from Django 7.0
Paginator(rows, per_page=2, orphans=20)

  ValueError: orphans cannot be larger
  than or equal to per_page
theorem page_bounded (hk : 0 < k) (ho : o < k) (l : List α) :
  ∀ p ∈ pages k o hk l, p.length < 2 * k
$ lake build
  Build completed successfully
$ lake build
  error: omega could not prove the goal
  a possible counterexample may satisfy
    a ≥ 1   a + b - c ≥ 0   2*a - c ≤ 0
  where  a := k  b := o  c := p.length

I am not asking you to prove everything

start at the boundary where your state meets something you do not control

A gateway that retries. A client that parses your JSON. A queue that delivers twice. That is where the contract lives, and it is small.

And you do not have to start with a proof

Every property on the last few slides can be checked without proving anything at all.

@given(st.lists(st.integers()), st.integers(min_value=1))
def test_pages_partition(rows, per_page):
    assert flatten(paginate(rows, per_page)) == rows
property-based testing fuzzing pip install hypothesis

They make you write the contract down. They do not make you discharge it. Most of the value, a fraction of the price, and you can do it this afternoon.

And it is not a silver bullet

A proof guarantees one thing: the code matches the spec.

this was never about certainty it is about how much confidence you can afford

All I am saying is that the price moved, and the economics are now on your side.

Thank you

LGTM is a vote. A theorem is a receipt.

Special thanks to Anirudh Gupta, IISc Bangalore for the initial idea of verifying across boundaries and

Prof. KC Sivaramakrishnan and Prof. Kartik Nagar for … well everything about Formal Verification.

Every tool in this talk is free software. Python · coverage.py · Hypothesis · Lean 4 · Django

? help