

Lean 4 from the perspective of program verification.
Introduction to Lean and dependent type theory — today
Programming and proving in Lean — inductive types, type classes, the tactic framework
Proof automation and AI — simp, grind, bv_decide, and what AI already does with Lean
Software verification in Lean — Hoare logic, a verification condition generator, and how it scales
Programs, specifications, and proofs in the same language.
Exercises: a self-contained Lean project, one file per lecture, with solutions.

What is Lean? A short history.
Getting started: installation, editor, where to find things.
What Lean is used for: mathematics, industry, AI. Facts and numbers.
Introduction to Dependent type theory.
every term has a type; propositions are types; proofs are programs
your first proofs, as terms and with tactics

A programming language and a proof assistant.
Same language for code, specifications, and proofs. No translation layer.
Small trusted kernel. Proofs can be exported and independently checked.
Lean is implemented in Lean. Very extensible.
~300,000 unique installations: VS Code (170K) + Open VSX (130K).
def reverse : List α → List α
| [] => []
| x :: xs => reverse xs ++ [x]
theorem reverse_append (xs ys : List α)
: reverse (xs ++ ys) = reverse ys ++ reverse xs := byα:Type u_1xs:List αys:List α⊢ reverse (xs ++ ys) = reverse ys ++ reverse xs
induction xs with
| nil =>α:Type u_1ys:List α⊢ reverse ([] ++ ys) = reverse ys ++ reverse [] simp [reverse]All goals completed! 🐙
| cons x xs ih =>α:Type u_1ys:List αx:αxs:List αih:reverse (xs ++ ys) = reverse ys ++ reverse xs⊢ reverse (x :: xs ++ ys) = reverse ys ++ reverse (x :: xs) simp [reverse, ih]All goals completed! 🐙
#eval[3, 2, 1] reverse [1, 2, 3]
[3, 2, 1]

Project started in 2013. Lean 4 officially released in 2023, implemented in Lean itself. The Lean FRO: a nonprofit, building Lean full-time since 2023.

"You have written my favorite computer game" — Kevin Buzzard, Prof. of Mathematics, Imperial College
def odd (n : Nat) : Prop := ∃ k, n = 2 * k + 1
theorem square_of_odd_is_odd : odd n → odd (n * n) := byn:Nat⊢ odd n → odd (n * n)
intro ⟨k₁, e₁⟩n:Natk₁:Nate₁:n = 2 * k₁ + 1⊢ odd (n * n)
simp [e₁, odd]n:Natk₁:Nate₁:n = 2 * k₁ + 1⊢ ∃ k, (2 * k₁ + 1) * (2 * k₁ + 1) = 2 * k + 1
exists 2 * k₁ * k₁ + 2 * k₁n:Natk₁:Nate₁:n = 2 * k₁ + 1⊢ (2 * k₁ + 1) * (2 * k₁ + 1) = 2 * (2 * k₁ * k₁ + 2 * k₁) + 1
liaAll goals completed! 🐙
The "game board": you see goals and hypotheses, then apply "moves" (tactics).
Each tactic transforms the game board.
The kernel checks the final proof term.

The elaborator turns what you write into fully explicit kernel terms.
The kernel — a small type checker — rechecks every definition and proof.
Tactics, simp, grind, AI-generated proofs: all produce terms the kernel checks.
Bugs in tactics cannot give you invalid theorems.
Proofs can be exported and rechecked by independent kernels (more on this in Lecture 3).
theorem two_ne_three : 2 ≠ 3 := by⊢ 2 ≠ 3 decideAll goals completed! 🐙
#print'two_ne_three' does not depend on any axioms axioms two_ne_three
#print axioms: what a proof ultimately depends on.

Parser, elaborator, compiler, tactic framework, build system: written in Lean.
Users extend Lean in Lean, at every level:
notation and macros (Lecture 2)
tactics (Lecture 4: we implement a verification condition generator)
elaborator, compiler
entire domain-specific languages
These slides are Lean files, written in Verso. Every code example you see is checked by Lean when the slides are built.

Recommended procedure: lean-lang.org/install
elan manages Lean versions. Projects pin their version in lean-toolchain;
the right compiler is downloaded automatically.
Editor: VS Code with the Lean 4 extension (also: Open VSX for VSCodium, Neovim, Emacs).
No installation at all: live.lean-lang.org runs full Lean in the browser.
For this course: clone the GitHub repository, open the folder in VS Code, done. The exercises use only the standard library — nothing to download beyond the toolchain.

The goal panel shows the game board: hypotheses and goals at the cursor.
Hover for types and documentation.
Errors appear as you type; the file is checked continuously.
#check, #eval, #print answer questions inline.


Theorem Proving in Lean 4 — this lecture follows its first chapters.
Functional Programming in Lean — Lean as a programming language (Lecture 2).
Lean language reference — the definitive manual.
Loogle — search the library by name or by type shape.
Lean Zulip — the community. Beginner questions welcome.
Natural Number Game — learn tactics as a game, in the browser.

An open-source, community-driven library. Today:
280,000+ formalized theorems.
2.4M+ lines of Lean. 50,000+ lines of extensions.
750+ contributors.
1,500+ type classes, 20,000+ instances.


Six Fields Medalists engaged: Tao, Scholze, Viazovska, Gowers, Hairer, Freedman.
Liquid Tensor Experiment (completed) — Commelin
The Polynomial Freiman-Ruzsa Conjecture (completed) — Tao
Equational Theories Project (completed) — Tao
Carleson's Theorem (completed) — van Doorn
Sphere Packing — Birkbeck, Hariharan, Lee, Ma, Mehta, Viazovska
Fermat's Last Theorem (in progress) — Buzzard
Inter-Universal Teichmüller Theory (in progress) — Mochizuki
At this scale, mathematics needs build systems, dependency graphs, code review, and a precise medium for communication.

November 2020: Peter Scholze posed a formalization challenge.
"I spent much of 2019 obsessed with the proof of this theorem, almost getting crazy over it. I still have some small lingering doubts." — Peter Scholze
Johan Commelin led a team that verified the proof, with only minor corrections.
"The Lean Proof Assistant was really that: an assistant in navigating through the thick jungle that this proof is." — Peter Scholze


Cedar — open-source authorization policy language. Used by AWS Verified Permissions and AWS Verified Access.
The model is written in Lean, alongside the Rust production code. The Lean model is ~10× smaller than the Rust implementation.

Verified components: evaluator, authorizer, validator.
~100M differential random tests nightly. Lean: 5 μs/test. Rust: 7 μs/test.
Release gate: no Cedar version ships unless model, proofs, and differential tests are current.


SymCrypt — Microsoft's core cryptographic library, rewritten in Rust.
Aeneas translates the safe Rust to pure functional Lean code.
The first release includes complete proofs for ML-KEM and SHA-3 code running in Windows Insider builds today.
"We are verifying code faster than we can write it." — Son Ho, SymCrypt, Microsoft

DEFLATE (the zlib format) implemented in Lean, proved correct.
theorem inflate_deflateRaw (data : ByteArray) (level : UInt8)
(maxOutputSize : Nat) (hsize : data.size ≤ maxOutputSize) :
inflate (deflateRaw data level) maxOutputSize = .ok data
On the silesia corpus (212 MB), lean-zip beats miniz_oxide — the standard pure-Rust implementation — at every compression level; 30% faster at the
default level 6.
The performance came from AI agents optimizing the code autonomously:
"The Lean library isn't just tested and validated, it's proved correct. This allows us to let AIs loose optimizing the code, requiring that they update the proof whenever the implementation materially changes." — Kim Morrison
Why Lean is faster than Rust, July 2026.

Every medal-level IMO AI with formal proofs uses Lean:
AlphaProof (Google DeepMind) — silver medal, IMO 2024
Aristotle (Harmonic) — gold medal, IMO 2025
Seed Prover (ByteDance) — silver medal, IMO 2025
AI plays the same game we saw earlier: goals, hypotheses, tactics.
The kernel checks every proof — an AI cannot "convince" Lean of a false statement without a kernel bug.
Much more on this in Lecture 3.

#check42 : Nat 42
42 : Nat#checkBool.true : Bool true
Bool.true : Bool#check"Marktoberdorf" : String "Marktoberdorf"
"Marktoberdorf" : String#check[1, 2, 3] : List Nat [1, 2, 3]
[1, 2, 3] : List Nat#check(2, true) : Nat × Bool (2, true)
(2, true) : Nat × Bool
#check e reports the type of e.
Every well-formed expression has a type.
The type checker is the entire game: a proposition will be a type, and checking a proof will be type checking.

def add (a b : Nat) : Nat := a + b
#eval5 add 2 3
5#checkadd (a b : Nat) : Nat add
add (a b : Nat) : Nat#checkadd 2 : Nat → Nat add 2
add 2 : Nat → Nat#checkfun n => n + 1 : Nat → Nat fun (n : Nat) => n + 1
fun n => n + 1 : Nat → Nat
add : Nat → Nat → Nat — functions of several arguments are curried:
add 2 is itself a function, Nat → Nat.
→ associates to the right: Nat → (Nat → Nat).
#eval runs programs; Lean is a compiled language (Lecture 2).

def twice (f : Nat → Nat) (x : Nat) : Nat := f (f x)
#eval16 twice (fun n => n + 3) 10
16def compose (f : β → γ) (g : α → β) (x : α) : γ := f (g x)
#check@compose : {β : Sort u_1} → {γ : Sort u_2} → {α : Sort u_3} → (β → γ) → (α → β) → α → γ @compose
@compose : {β : Sort u_1} → {γ : Sort u_2} → {α : Sort u_3} → (β → γ) → (α → β) → α → γ
Functions take functions and return functions.
α, β, γ are implicit type arguments — Lean inserts and infers
them. @compose shows the full type.

#checkNat : Type Nat
Nat : Type#checkNat → Nat : Type Nat → Nat
Nat → Nat : Type#checkList.{u} (α : Type u) : Type u List
List.{u} (α : Type u) : Type u#checkList Nat : Type List Nat
List Nat : Type#checkType : Type 1 Type
Type : Type 1#checkType 1 : Type 2 Type 1
Type 1 : Type 2
Types are ordinary terms: Nat : Type, and Type : Type 1, ... —
an infinite hierarchy of universes.
List : Type → Type is a function on types.
No special "type language": one language for everything.

#checkFin (n : Nat) : Type Fin
Fin (n : Nat) : Type#checkBitVec (w : Nat) : Type BitVec
BitVec (w : Nat) : Type#check7 : Fin 10 (7 : Fin 10)
7 : Fin 10def zeros (n : Nat) : BitVec n := 0
#checkzeros (n : Nat) : BitVec n zeros
zeros (n : Nat) : BitVec n
Fin 10: natural numbers below 10. BitVec 32: bitvectors of width 32.
zeros : (n : Nat) → BitVec n — the result type depends on the argument
value. This is a dependent function type.
This is the "dependent" in dependent type theory, and it is what lets types express specifications.

#check2 + 2 = 4 : Prop 2 + 2 = 4
2 + 2 = 4 : Prop#check2 + 2 = 5 : Prop 2 + 2 = 5
2 + 2 = 5 : Prop#check∀ (n : Nat), 0 ≤ n : Prop ∀ n : Nat, 0 ≤ n
∀ (n : Nat), 0 ≤ n : Prop#checkNat → Nat : Type Nat → Nat
Nat → Nat : Type
Prop is the universe of propositions.
2 + 2 = 5 is a perfectly well-formed proposition. Stating is not proving.
A proposition is a type; its inhabitants are its proofs.
A false proposition is an empty type: no proof exists.

def identity (α : Type) (a : α) : α := a
theorem p_imp_p (p : Prop) (h : p) : p := h
The same shape: given a type and an element, return an element.
theorem is def — a proof is a term, checked by the same kernel that
checks programs.
This is the Curry–Howard correspondence.

theorem modus_ponens (p q : Prop) (hpq : p → q) (hp : p) : q :=
hpq hp
theorem imp_trans (p q r : Prop) (h₁ : p → q) (h₂ : q → r) : p → r :=
fun hp => h₂ (h₁ hp)
A proof of p → q is a function from proofs of p to proofs of q.
Modus ponens is function application.
Transitivity of implication is function composition.

structure And (a b : Prop) : Prop where
intro ::
left : a
right : b
example (p q : Prop) (hp : p) (hq : q) : p ∧ q := And.intro hp hq
example (p q : Prop) (hp : p) (hq : q) : p ∧ q := ⟨hp, hq⟩
example (p q : Prop) (h : p ∧ q) : q ∧ p := ⟨h.right, h.left⟩
example (p q : Prop) (h : p ∧ q) : q ∧ p := ⟨h.2, h.1⟩
∧ is not built in — it is an ordinary structure (this is its actual
definition).
⟨_, _⟩ is the anonymous constructor; h.left and h.right are
projections. A proof of a conjunction is a pair.

inductive Or (a b : Prop) : Prop where
| inl (h : a)
| inr (h : b)
example (p q : Prop) (h : p ∨ q) : q ∨ p :=
match h with
| .inl hp => .inr hp
| .inr hq => .inl hq
Two constructors: a proof of p ∨ q carries a proof of one side.
Using a disjunction is pattern matching — case analysis is match.
Inductive types are the topic of Lecture 2; Or is a first example.

inductive False : Prop
def Not (a : Prop) : Prop := a → False
example (p : Prop) : ¬(p ∧ ¬p) :=
fun h => h.right h.left
example (p : Prop) : ¬(p ∧ ¬p) :=
fun h : p ∧ ¬p =>
have h₁ : p := h.left
have h₂ : ¬p := h.right
h₂ h₁
False is an inductive type with no constructors — no proof exists.
¬p is just p → False. The example above is function application again.
From False everything follows: False.elim : False → C.

example (p q : Nat → Prop) (h : ∀ x, p x ∧ q x) : ∀ x, p x :=
fun x => (h x).left
#check∀ (n : Nat), 0 ≤ n : Prop ∀ n : Nat, 0 ≤ n
∀ (n : Nat), 0 ≤ n : Prop#check∀ (n : Nat), 0 ≤ n : Prop (n : Nat) → 0 ≤ n
∀ (n : Nat), 0 ≤ n : Prop
∀ x : α, p x is the dependent function type (x : α) → p x —
the same construct as zeros : (n : Nat) → BitVec n.
To prove: a function taking each x to a proof. To use: apply it.
Nothing new was added: quantifiers were already in the type system.

example : ∃ n : Nat, n * n = 25 := ⟨5, rfl⟩
example (p : Nat → Prop) (h : ∃ x, p x ∧ x > 0) : ∃ x, p x :=
match h with
| Exists.intro x (And.intro hp _) => Exists.intro x hp
example (p : Nat → Prop) (h : ∃ x, p x ∧ x > 0) : ∃ x, p x :=
match h with
| ⟨x, hp, _⟩ => ⟨x, hp⟩
A proof of ∃ x, p x is a pair: a witness and a proof for it.
To use one, pattern match — like And, with a twist: the witness may
only be used to prove propositions (next slide).

theorem proofs_are_equal (p : Prop) (h₁ h₂ : p) : h₁ = h₂ := rfl
def Positive : Type := { n : Nat // 0 < n }
def three : Positive := ⟨3, by⊢ 0 < 3 decideAll goals completed! 🐙⟩
#eval3 three.val
3
Proof irrelevance: any two proofs of a proposition are equal — only that a proposition holds matters, never which proof.
Proofs are erased by the compiler: three is just 3 at runtime.
Specifications cost nothing at runtime.
{ n : Nat // 0 < n } is a subtype: a value bundled with a proof —
the shape of requires/ensures contracts in Lecture 4.

example : 2 + 2 = 4 := rfl
example (a b c : Nat) (h₁ : a = b) (h₂ : b = c) : a = c :=
h₁.trans h₂
example (f : Nat → Nat) (a b : Nat) (h : a = b) : f a = f b :=
congrArg f h
rfl proves a = b when both sides compute to the same value —
definitional equality.
Eq is reflexive, symmetric (h.symm), transitive (h.trans), and
congruent (congrArg).
Eq is an inductive type too.
inductive Eq {α : Sort u} (a : α) : α → Prop
| refl : Eq a a
theorem Eq.symm (h : Eq a b) : Eq b a :=
match h with
| .refl => .refl

def Eq {α : Sort u} (a b : α) : Prop :=
∀ (p : α → Prop), p a → p b
theorem Eq.refl {α : Sort u} (a : α) : Eq a a :=
fun (p : α → Prop) (h : p a) => h
theorem Eq.symm {α : Sort u} {a b : α} (h : Eq a b) : Eq b a :=
fun (p : α → Prop) (hb : p b) => h (fun x => p x → p a) id hb
theorem Eq.trans {a b c : α} (h₁ : Eq a b) (h₂ : Eq b c) : Eq a c :=
fun (p : α → Prop) (ha : p a) => h₂ p (h₁ p ha)
theorem congrArg (f : α → β) (a b : α) (h : Eq a b) : Eq (f a) (f b) :=
fun (p : β → Prop) (hp : p (f a)) => h (fun x => p (f x)) hp

example (p q : Prop) (hp : p) (hq : q) : p ∧ q := ⟨hp, hq⟩
example (p q : Prop) (hp : p) (hq : q) : p ∧ q := byp:Propq:Prophp:phq:q⊢ p ∧ q
constructorp:Propq:Prophp:phq:q⊢ pp:Propq:Prophp:phq:q⊢ q
exact hpp:Propq:Prophp:phq:q⊢ q
exact hqAll goals completed! 🐙
by enters tactic mode: you see hypotheses and goals, and apply moves.
The two proofs above produce the same term — tactics are programs that construct proof terms.
Term mode and tactic mode mix freely; use whichever is clearer.

intro, apply, exactexample (p q r : Prop) (hpq : p → q) (hqr : q → r) : p → r := byp:Propq:Propr:Prophpq:p → qhqr:q → r⊢ p → r
intro hpp:Propq:Propr:Prophpq:p → qhqr:q → rhp:p⊢ r
apply hqrp:Propq:Propr:Prophpq:p → qhqr:q → rhp:p⊢ q
apply hpqp:Propq:Propr:Prophpq:p → qhqr:q → rhp:p⊢ p
exact hpAll goals completed! 🐙
intro hp — move the hypothesis of an implication (or ∀) into context.
apply hqr — work backwards: to prove r, it suffices to prove q.
exact hp — the goal is exactly this term.
Compare with the term proof fun hp => hqr (hpq hp): same structure,
written back-to-front.

cases and obtainexample (p q : Prop) (h : p ∨ q) : q ∨ p := byp:Propq:Proph:p ∨ q⊢ q ∨ p
cases h with
| inl hp =>p:Propq:Prophp:p⊢ q ∨ p exact Or.inr hpAll goals completed! 🐙
| inr hq =>p:Propq:Prophq:q⊢ q ∨ p exact Or.inl hqAll goals completed! 🐙
example (p : Nat → Prop) (h : ∃ x, p x ∧ x > 0) : ∃ x, p x := byp:Nat → Proph:∃ x, p x ∧ x > 0⊢ ∃ x, p x
obtain ⟨x, hp, _⟩ := hp:Nat → Propx:Nathp:p xright✝:x > 0⊢ ∃ x, p x
exact ⟨x, hp⟩All goals completed! 🐙
cases is match in tactic mode, one goal per constructor.
obtain ⟨x, hp, _⟩ destructures in one step — the workhorse for ∃ and ∧.

example : 123 * 456 = 56088 := by⊢ 123 * 456 = 56088 decideAll goals completed! 🐙
example (n : Nat) : n + 0 = n := byn:Nat⊢ n + 0 = n simpAll goals completed! 🐙
example (a b c : Nat) : a + b + c = c + b + a := bya:Natb:Natc:Nat⊢ a + b + c = c + b + a grindAll goals completed! 🐙
decide — evaluate a decidable proposition.
simp — rewriting with a database of equations.
grind — Lean's general-purpose automation.

sorry and #printtheorem and_swap (p q : Prop) (h : p ∧ q) : q ∧ p := ⟨h.right, h.left⟩
#printtheorem and_swap : ∀ (p q : Prop), p ∧ q → q ∧ p :=
fun p q h => ⟨h.right, h.left⟩ and_swap
theorem and_swap : ∀ (p q : Prop), p ∧ q → q ∧ p :=
fun p q h => ⟨h.right, h.left⟩
exampledeclaration uses `sorry` (p q r : Prop) (h : (p ∧ q) ∧ r) : p ∧ (q ∧ r) := byp:Propq:Propr:Proph:(p ∧ q) ∧ r⊢ p ∧ q ∧ r
sorryAll goals completed! 🐙
sorry closes any goal and reports a warning: sketch first, prove later.
Every exercise file compiles from the start — sorry marks your work.
#print shows the proof term a tactic built.

Logic | Type theory |
|---|---|
proposition | type |
proof | term |
| function type |
| dependent function type |
| structure (pair) |
| inductive type (two constructors) |
|
|
| witness–proof pair |
One system. The kernel only ever checks types.

Exercises/Lecture1.lean — ordered by difficulty, sorry-driven:
Small functions, checked by #guard.
Propositional logic: each lemma twice — as a term, then with tactics.
Quantifiers.
Equality and calc.
Formalize statements without proving them (one is false, one is open).
The barber paradox.
Hints point at apply?, simp?, and Loogle. Solutions with notes are in
Solutions/.
No installation? live.lean-lang.org.

Lean: one language for programs, specifications, and proofs; a small kernel checks everything.
Real adoption: Mathlib, AWS, Google, Meta, Microsoft, several startups, every medal-level IMO AI.
Dependent type theory: propositions are types, proofs are programs; quantifiers are dependent function types.
Tactics construct proof terms; the kernel checks them.
Next lecture: inductive types, type classes, Lean as a programming language — and proofs about programs.
