Part 5 — Dependent-Type Methods

The theoretical heart of Deppy — why homotopy type theory is the right foundation for verifying Python.

Why Homotopy Type Theory for Python?

Every formal verification tool must answer a deceptively simple question: what does it mean for two things to be "the same"?

In most languages this has a straightforward answer. In Haskell, two values are equal when == returns True. In Java, .equals() decides it. But Python is different. Python lives in a world of duck typing, protocols, dynamic dispatch, and multiple inheritance — a world where "sameness" is nuanced, contextual, and structural.

Duck typing is the idea that objects are interchangeable if they behave the same, regardless of their concrete class. A list and a tuple are different types, yet both support indexing, iteration, and len(). In a duck-typed world they are "the same" for code that only uses those operations.

Traditional type theories — System F, dependent types à la Coq, even refinement types — model equality as a proposition: either two things are equal or they are not. This binary view cannot capture duck typing. A list is not literally a tuple, yet they are interchangeable in many contexts. We need a notion of equality that is evidence-bearing and context-sensitive.

Homotopy type theory (HoTT) provides exactly this. In HoTT, equality is not a mere proposition — it is a type whose inhabitants are paths. A path p : a =A b is evidence that a and b are connected. There can be many different paths (different reasons two things are equal), and those paths can themselves be compared via higher paths.

The Deep Insight: Duck Typing = Path Equivalence

Here is the key observation that makes Deppy possible:

Two Python objects are "the same" for a given protocol precisely when there exists a path between them in the type that the protocol defines.

Consider a protocol Iterable. A list and a range object are different types, but both support __iter__. In Deppy this is modeled as:

# Two concrete types
my_list  : list[int]   = [1, 2, 3]
my_range : range        = range(1, 4)

# Path evidence: they are equal *as Iterables*
p : PathType(Iterable[int], my_list, my_range)
p = duck_path(Iterable[int], my_list, my_range)

# Now any proof about my_list-as-Iterable transports to my_range
proof_range = transport(property, p, proof_list)

This is not merely an analogy. The duck_path constructor builds a genuine proof term that two objects satisfy the same protocol operations, and transport carries proofs along that path. The path might be non-trivial — it records exactly how the two objects relate.

Types as Spaces, Programs as Points, Equalities as Paths

The homotopy interpretation gives us three fundamental correspondences:

TopologyType TheoryPython
Space Type Class / Protocol
Point Term / Value Object instance
Path (continuous map I → X) Evidence of equality a =A b Duck-typing witness, protocol conformance proof
Homotopy (path between paths) Higher equality Proof that two refactorings are equivalent
Fibration Dependent type family Class hierarchy, Generic[T]
Fiber Type at a specific index Concrete subclass, Generic[int]
Section Dependent function Method that works for all subclasses
Čech cover Case decomposition if/elif/else, match/case

This table is not window dressing — every row corresponds to a concrete data structure and algorithm inside Deppy. When you write a @guarantee-annotated Python function, Deppy literally constructs spaces, paths, fibrations, and Čech covers to verify it.

A Topological View of a Python Program

Think of a Python function's type signature as a space. The function itself is a point in the space of all functions with that signature. Two implementations of the same interface are two points in the same space, and a proof that they behave the same is a path connecting them.

# Two sorting algorithms — two points in the same space
def bubble_sort(xs: list[int]) -> list[int]: ...
def merge_sort(xs: list[int]) -> list[int]: ...

# A path between them: proof that ∀xs. bubble_sort(xs) = merge_sort(xs)
# This is a continuous family of equalities, not a single bit.
p : PathType(
    (list[int] -> list[int]),
    bubble_sort,
    merge_sort
)
# Constructed via function extensionality (FunExt)
p = funext(lambda xs: sort_equivalence_proof(xs))

The Four Key Methods

Part 5 introduces the four dependent-type methods that power Deppy's verification engine. Each addresses a different aspect of Python verification, and together they cover the full landscape of real-world Python code.

🔵 Path Types & Transport

The foundation. Paths model equality-as-evidence. Transport carries proofs along paths. Function extensionality and univalence complete the picture. This is how Deppy verifies that two implementations are interchangeable.

Read chapter →

🟣 Čech Decomposition

Decompose a function into patches (branches, cases, exception handlers), verify each independently, then glue the local proofs into a global guarantee using Čech cohomology.

Read chapter →

🟢 Fibrations

Model class hierarchies, generic types, and module structures as fibrations. Verify properties per-fiber (per-subclass) then conclude global correctness via the fibration structure.

Read chapter →

🟪 PSDL — Python-syntax Tactics

The proof body is ordinary Python: walrus binding, postfix .symm/.trans, operator overloading p @ q / ~p / p ** k, comprehensions as induction, and assert P, "qed" as proof closer. Every line maps to a kernel ProofTerm.

Read chapter →

🟡 Homotopy Tactics

The kernel-level tactic engine. Homotopy-native tactics like transport, path_induction, and cech_decompose. PSDL emits these tactics; this chapter is their reference manual.

Read chapter →

Beyond F*, Dafny, and Liquid Haskell

To appreciate what Deppy brings, let's compare with the state of the art in formal verification:

Feature F* Dafny Liquid Haskell Deppy
Dependent types ✅ Full ✅ Refinement ✅ Full + homotopy
Duck typing / structural eq. ✅ Path types
Branch decomposition Manual Manual N/A ✅ Čech automatic
Class hierarchy verification Limited Traits N/A ✅ Fibrations
Proof reuse via equivalence Limited ✅ Transport + univalence
Dynamic typing ✅ Graded modality
Target language F* / OCaml Dafny / C# Haskell Python

The key differentiator is not any single feature but the combination: path types for structural equality, Čech decomposition for branching code, fibrations for class hierarchies, and transport for proof reuse — all working together in a cubical type theory that naturally accommodates Python's dynamic, duck-typed semantics.

Deppy does not require you to learn homotopy type theory. The surface language (@guarantee, assume, check, hole) hides the topology. But understanding the theory lets you write more powerful specifications, debug verification failures, and appreciate why certain proofs succeed or fail.

Why Cubical Homotopy Type Theory?

Deppy is built on cubical type theory (CCHM / Cartesian cubical), not classical HoTT. The distinction matters:

For a verification tool this is crucial. When Deppy transports a proof from list to tuple, it needs to compute the adapted proof term. Cubical paths make this possible.

# In cubical Deppy, a path is a function from the interval
# PathAbs(i, body) — at i=0 we get the left endpoint, at i=1 the right

p = PathAbs("i", lambda i:
    # Interpolate between list and tuple representations
    interp(list_repr, tuple_repr, i)
)

# PathApp evaluates the path at a specific point
PathApp(p, 0)   # → list_repr   (left endpoint)
PathApp(p, 1)   # → tuple_repr  (right endpoint)
PathApp(p, 0.5) # → midpoint    (computes!)

What You'll Learn

The five chapters in Part 5 build on each other:

  1. Path Types & Transport — the foundation. You'll learn what paths are, how to construct them (Refl, Sym, Trans, Cong, Ap, FunExt), and how transport carries proofs along them. This chapter also covers univalence, the most powerful principle in HoTT.
  2. Čech Decomposition — decomposing branching Python code into patches and gluing local proofs into global correctness. You'll see how if/elif/else, match/case, and try/except are all instances of the same topological construction.
  3. Fibrations — modeling class hierarchies, generics, and module structures as fiber bundles. You'll learn how to verify a property per-subclass and conclude it holds globally.
  4. PSDL: Python-syntax Tactics — Deppy's tactic language, whose source is just Python. Walrus binding, postfix navigation on equalities, operator overloading (p @ q, ~p, f * p, p ** k), comprehensions as induction, assert with proof tag (assert P, "qed"), and the cubical-effect mapping table tying it all to the kernel.
  5. Homotopy Tactics — the kernel-level tactic engine that PSDL targets. Includes homotopy-native tactics that have no analogue in other systems.
Prerequisites. You should be comfortable with Parts 1–4 before diving into Part 5. In particular, you should understand refinement types (Part 1), dependent types (Part 2), and Python protocols (Part 3). No prior knowledge of topology or homotopy theory is required — we build everything from scratch.

The Mental Model

Before we dive into the chapters, here is the mental model to carry with you throughout Part 5:

# The verification pipeline (conceptual)

# 1. Parse Python → extract type information
space   = type_to_space(function_signature)

# 2. Function body → point in the space
point   = term_to_point(function_body)

# 3. Specification → target region of the space
target  = spec_to_region(guarantee_annotation)

# 4. Verify: construct a path from point to target
proof   = find_path(point, target, space)
#          ↑ uses Čech decomposition, fibrations, transport

# 5. If proof exists → verified ✅
#    If no path can be found → report the obstruction
#    (The obstruction is a cohomology class — a witness
#     explaining WHY the proof fails.)

This is the engine that runs under every @guarantee annotation. The surface is simple; the topology underneath is what makes it powerful.

A Taste of What's Coming

To whet your appetite, here is a small example that uses paths, transport, and Čech decomposition together:

from deppy import guarantee, check, assume

@guarantee("returns absolute value: result >= 0 and (result == x or result == -x)")
def my_abs(x: int) -> int:
    if x >= 0:
        return x       # Patch U₁: x ≥ 0
    else:
        return -x      # Patch U₂: x < 0

Deppy verification trace

1
Čech decomposition — split into two patches: U₁ = {x | x ≥ 0} and U₂ = {x | x < 0}. These cover all integers.
2
Local verification — on U₁: result = x ≥ 0 ✓ and result == x ✓. On U₂: result = -x > 0 ✓ and result == -x ✓. Both patches satisfy the guarantee.
3
Overlap checkU₁ ∩ U₂ = ∅ (no integer is both ≥ 0 and < 0). No gluing condition to check.
4
Čech glueCechGlue(proof₁, proof₂) produces a global proof. Verified

Now suppose we have a second absolute-value implementation:

def my_abs2(x: int) -> int:
    return x if x >= 0 else -x

Deppy constructs a path between my_abs and my_abs2 via function extensionality (they agree on all inputs), then transports the verified guarantee along the path — so my_abs2 is verified for free.

Think about it. Can you think of a Python function where the branches overlap — where the same input could trigger two different cases? How would you verify the guarantee holds on the overlap region? (Hint: this is exactly the Čech gluing condition.)

Notation Guide

Throughout Part 5 we use the following notation:

NotationMeaning
a =A bPath type: evidence that a and b are equal in type A
refl(a)Reflexivity path: a = a
p · qPath composition: if p: a=b and q: b=c, then p·q: a=c
p⁻¹Path inverse: if p: a=b, then p⁻¹: b=a
transport(P, p, x)Carry x : P(a) along p : a=b to get P(b)
A ≃ BEquivalence: a bijection with both-sided inverse
Σ(x:A). B(x)Dependent pair (total space of a fibration)
Π(x:A). B(x)Dependent function (section of a fibration)
𝕀The interval type [0, 1] in cubical type theory
UiPatch / open set in a Čech cover

Ready? Let's begin with the most important chapter in the entire book: Path Types & Transport →