Skip to main content

forall

A sound, non-executing verifier for type-annotated Python. forall reads your source, lowers each function's logic to SMT (via pysmt + Z3), and either finds a replayable crash or proves crash-freedom: without ever importing or running your code. Analysing a module that opens sockets, spawns processes, or deletes files is completely safe: there is nothing to sandbox.

Its cardinal guarantee is zero false verdicts: every reported crash comes with a concrete input that actually raises it, and every proof holds for every input the types (or your assumptions) admit. When it can't decide something, it says so; it never guesses.

forall adapts the Kani model checker's methodology from Rust to typed Python. Like Kani, it offers push-button crash-finding and a specification language (proof harnesses, loop invariants, and function contracts), so you can start with zero annotation and scale up to unbounded correctness proofs as needed.

Install

uv sync                # or: uv pip install -e .

Requires Python 3.13. (Z3 is pinned to the last release with a prebuilt wheel for the project's Python/arch; see pyproject.toml.)

Quick start

Point it at a file or directory (directories are walked for *.py):

uv run forall check path/to/module.py
uv run forall check src/                    # walk a whole tree
uv run forall check module.py --conditional # decide more, modulo external calls
uv run forall check a.py b.py --json        # machine/AI-readable output
uv run forall check module.py -dd           # trace the analysis (to stderr)

Given this file:

from forall.harness import ensures, requires


def average(total: int, count: int) -> int:
    return total // count


@requires(lambda qty: qty >= 1)
@ensures(lambda qty, result: 1 <= result <= qty)
def clamp_batch(qty: int) -> int:
    if qty > 100:
        return 100
    return qty


def dispatch(qty: int) -> int:
    return clamp_batch(qty)

forall reports:

VERIFIED — 3 of 3 functions (100%)

CRASHES — 1, each with an input that triggers it
  orders.py:5: average: ZeroDivisionError: integer division or modulo by zero
      reproduce: average(0, 0)

  proven crash-free — 1, for every input their types allow
    orders.py
        dispatch
  proven against their contracts — 1, for every input the @requires admits
    orders.py: clamp_batch
        qty >= 1
        1 <= result <= qty

average(0, 0) really does raise. clamp_batch is proven to meet its contract for every valid input, and because that proof exists, dispatch gets it for free. Its body was never re-analysed. Exit code is 1 when any crash is found, 0 otherwise, so it drops into CI like a linter.

The things it can do

Mode What you write What you get
Crash-finding (default) nothing Sound, replayable crashes + proofs of crash-freedom for whatever it can fully model.
--unwind K nothing Bounded model checking: unrolls each loop K times to find crashes inside loops, with the exact witness. Additive: never costs you a proof.
--conditional nothing Also decides functions whose only unknown is an external call, assuming those calls return normally. Verdicts tagged CONDITIONAL.
Proof harnesses a @proof function Prove a real property (validate_port(p) == p for all valid p), beyond just crash-freedom. Loop invariants prove properties over loops of any length; char-level and composition reasoning proves a validator's security invariants.
Contracts @requires / @ensures Verify a function once against a spec, then reuse the contract at every call site, skipping re-analysis of the body. This is how verification scales.

They share one report, the ledger (below), and one guarantee: no false crash, no false proof.

Work through the core arc in docs/src/tutorial.md: eight steps, one file each, in examples/.

The report is a ledger

The headline is two numbers over the whole codebase. VERIFIED counts what forall could decide on its own. SPECIFIED counts what is proven against a contract or @proof property you wrote: the number that says the code is correct.

VERIFIED — 12 of 225 functions (5%)
SPECIFIED — 0 of 225 functions (0%), proven against a contract or a @proof property
  → nothing here states what the code should DO; crash-free is not correct

CRASHES — 2, each with an input that triggers it
  ...file:line, exception, and a REAL reproducer each; -v adds the source line...

  proven crash-free — 10, for every input their types allow
    (run with -v to list every proven function by name)

UNVERIFIED — 213 of 225 functions (95%). forall makes NO claim about these.
  → --conditional decides 31 of them right now.

  YOUR MOVE — 29
      29  external call — body not available
          → --conditional, or call it from a @proof harness

  FORALL OWES YOU — 184  (ranked by the coverage each would buy)
     111  objects — attribute reads and method calls   [37% of the gap]
      ...

The unverified remainder is a burn-down with an owner on every line: what you can do (run --conditional/--unwind, write a harness, add an invariant) versus what forall still can't model, ranked by how much coverage each feature would buy. A percentage with no next action is a shrug; a next action with no percentage is a to-do list nobody starts. The report gives both, and it never lets "no crashes found" be mistaken for "your code is safe". It always says how much it actually looked at.

Evaluating it

Run the tests and the lint gate:

uv run pytest         # 900+ unit/integration/e2e tests
make lint             # ruff (format + 88-col) + mypy + ty; zero warnings

Check the soundness guarantee yourself. forall's verdicts are validated by generate-then-execute red-teaming, never by eyeballing. Adversarial programs (LLM-generated, designed to trick the verifier) are stored as JSON corpora in sandbox/; a deterministic ground-truth harness runs every function and every reproducer and flags any disagreement. One command routes every corpus to the tier that decides it:

make red-team

It prints, per corpus, PASS — 0 unsound or names the offending verdict with the input that falsifies it, then an aggregate. The current state: ~4,700 adversarial programs across ~70 corpora, all 5 tiers, 0 unsound. (sandbox/ is gitignored; the corpora live locally.)

The sandbox/redteam_*.mjs files are the generators that produced those corpora (multi-agent adversarial generation).

Run it against the whole Python standard library. The stdlib is the standing external benchmark: make stdlib-sweep verifies all ~14,500 functions in ~30 s, validates every crash claim by executing its reproducer, executes hundreds of its own proofs under type-valid draws, and diffs every claim against the committed baseline (notes/13-STDLIB-LEDGER.md). A lost proof is named, audited, and accepted or fixed. It has found real stdlib crashes (urllib.parse._coerce_args(), every curses.ascii predicate on ''), each reproduced live before entering the ledger.

Where to look next

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

forall-0.1.1.tar.gz (219.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

forall-0.1.1-py3-none-any.whl (235.7 kB view details)

Uploaded Python 3

File details

Details for the file forall-0.1.1.tar.gz.

File metadata

  • Download URL: forall-0.1.1.tar.gz
  • Upload date:
  • Size: 219.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.33 {"installer":{"name":"uv","version":"0.11.33","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for forall-0.1.1.tar.gz
Algorithm Hash digest
SHA256 3234603c2e631395b897fdf4042bf804fb8474efafca59dd7ff1887ae9d58f80
MD5 6f01e62f24082f5093fceea766441a46
BLAKE2b-256 a917049f9f8424760b699ad12cbcf56088668bbdd64f440bed509873cdbdc3be

See more details on using hashes here.

File details

Details for the file forall-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: forall-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 235.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.33 {"installer":{"name":"uv","version":"0.11.33","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for forall-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 4d634f161d1e9084cafe076bdfceae8f3a0d5b83c2b7cc2c542fa4c137468a36
MD5 63a6160c9bcb653e495b31b0794ffc63
BLAKE2b-256 2e8db290a539ee03be28ca0f5783da048620f4781dd86ce7224119c9b1ce7738

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page