Skip to main content

Bidirectional Lean4-Python transpiler

Project description

LP² (lp2) — Lean ↔ Python, squared

LP² = LP² = LP × LP = Lean ↔ Python, squared for bidirectional. Translates in both directions: Python→Lean and Lean→Python.

Python Ruff

Translate Python functions with type hints into Lean 4 definitions, and Lean 4 definitions into Python functions.

Install

pip install lp2

Usage

CLI

# Python → Lean4
lp2 py2lean my_file.py
lp2 py2lean --stdin < my_file.py

# Lean4 → Python
lp2 lean2py my_file.lean
lp2 lean2py --stdin < my_file.lean

Python API

from lp2 import py_to_lean, lean_to_py, convert_str, convert_file

lean_code = py_to_lean("def add(x: int, y: int) -> int:\n    return x + y\n")
py_code = lean_to_py("def add (x y : Int) : Int := x + y\n")

# Convenience
result = convert_str(source, "py2lean")
result = convert_file("input.py", "py2lean")

Examples

For more comprehensive examples, see the examples/ directory:

  • demo_factorial_roundtrip.py / factorial.py: Recursive factorial (Python → Lean → Python)
  • demo_fibonacci_roundtrip.py / fibonacci.py: Recursive Fibonacci
  • demo_bubble_sort_roundtrip.py / bubble_sort.py: Bubble sort with nested while loops
  • demo_quicksort_roundtrip.py / quicksort.py: Quicksort with for-loop + if/elif/else
  • demo_fermat_roundtrip.py / fermat.py: Fermat factorization with while loops
  • demo_theorem_transpilation.py: Transpile Lean theorems to Python as Boolean-valued functions
  • factorial_algorithms.py / factorial_algorithms_test.py / factorial_algorithms.lean: Multiple factorial algorithms (iterative, product tree, prime swing) with @no_transpile for unsupported features
  • simple_while.py: Simple while loop example

Python → Lean4:

def fib(n: int) -> int:
    if n <= 1:
        return n
    return fib(n - 1) + fib(n - 2)
def fib (n : Int) : Int :=
  if n  1 then n else fib (n - 1) + fib (n - 2)

Lean4 → Python:

def isZero (n : Int) : Bool := match n with
  | 0 => true
  | _ => false
def isZero(n: int) -> bool:
    match n:
        case 0:
            return True
        case _:
            return False

Caveats & Limitations

no_transpile — skipping unsupported Python features

When the transpiler encounters a Python feature it cannot translate (e.g., generators, comprehensions, bytearray, f-strings), it raises an error by default. You can mark any function or statement with @no_transpile to keep the original Python source in the output as a Lean comment:

@no_transpile
def sieve(n: int) -> list[int]:
    bs = bytearray(b"\x01") * (n + 1)
    bs[0:2] = b"\x00\x00"
    return [i for i in range(2, n + 1) if bs[i]]

Generates:

-- no_transpile (@sieve)
--   def sieve(n: int) -> list[int]:
--       bs = bytearray(b"\x01") * (n + 1)
--       ...

For individual lines, use a # no_transpile comment:

sys.set_int_max_str_digits(100000)  # no_transpile

Both annotations are parser-only and never executed at Python runtime.

Stdlib source-following for imported functions

When the transpiler sees import math, it imports the module at transpile time. If it encounters math.factorial(x), it attempts to retrieve the function's source via inspect.getsource and transpile it automatically. For C-extension functions (like math.factorial), where no Python source exists, it falls back to transpiling a known Python implementation:

partial def factorial (n : Nat) : Nat :=
  let result := 1; let rec loop (i : Nat) (result : Nat) : Nat :=
    if i < n + 1 then loop (i + 1) (result * i) else result; loop 2 result

This avoids maintaining a brittle mapping from Python stdlib names to Lean library paths. Pure-Python functions from functools, collections, etc. can be auto-transpiled in the same way. Only functions where source retrieval fails and no fallback exists require @no_transpile.

Termination on Int recursion

Lean's termination checker cannot prove well-foundedness for recursion over Int. The transpiler auto-detects self-recursive calls and emits partial def for such functions. This sidesteps termination checking but means Lean cannot verify the function always halts.

def factorial(n: int) -> int:
    if n <= 1:
        return 1
    return n * factorial(n - 1)
partial def factorial (n : Int) : Int :=
  if n  1 then 1 else n * factorial (n - 1)

+ on lists (Python concatenation vs. Lean element-wise addition)

Python's + on lists means concatenation. Lean 4.30.0-rc2 defines Add (List α) as element-wise addition (zipWith). The transpiler emits a local instance that overrides Add (List Int) to use List.append, so + on List Int concatenates as expected. This only covers List Int — other list types (e.g., List Float, List String) remain element-wise.

List mutation is silently dropped

In-place list mutation (xs[i] = x, xs.append(x)) is not supported. The transpiler silently drops these statements. Use immutable patterns instead:

# Instead of:
#   xs[i] = x
#   xs.append(x)

# Use:
#   xs = xs[:i] + [x] + xs[i+1:]
#   xs = xs + [x]

Round-trip is lossy

Transpiling Python → Lean → Python recovers syntactically valid Python code but loses some information:

Lost detail Example Round-trip result
// (int division) x // y x / y
tuple type (1, 2)Prod.mk parsed as generic call
Return type annotations def f() -> int: def f():
for / while loops let rec / match parser skips (not yet supported)
Nested let chains let a := ...; let b := ... may produce unusual def wrappers

No Std or mathlib4 available

The transpiler targets Lean 4.30.0-rc2 without Std or mathlib4. Features like omega, List.range, HashMap, Nat.factorial, and Nat.popCount are unavailable from the standard library. The transpiler compensates with:

  • A built-in partial def get helper for list indexing (bypassing Fin proof requirements)
  • A built-in Nat.popCount shim (bit-count via recursion)
  • Stdlib source-following (see above) to synthesize implementations from equivalent Python code
  • @no_transpile as the escape hatch for anything that cannot be synthesized

Unsupported Python features

These Python constructs have no Lean equivalent or are not yet implemented:

  • Generators / yield
  • async / await
  • Decorators (except the special @no_transpile)
  • Exception handling (try/except/finally)
  • with statements (context managers)
  • List/dict/set comprehensions
  • *args / **kwargs (variadic functions)
  • Classes with methods (only simple data structures supported)
  • Augmented assignment on lists (xs += [x])
  • F-strings, slice assignment, dict/set mutation

Development

pip install -e ".[test]"
pytest
ruff format src/ tests/
prospector --with-tool ruff --with-tool mypy src/

License

MIT

Project details


Download files

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

Source Distribution

lp2-0.1.2.tar.gz (39.2 kB view details)

Uploaded Source

Built Distribution

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

lp2-0.1.2-py3-none-any.whl (44.6 kB view details)

Uploaded Python 3

File details

Details for the file lp2-0.1.2.tar.gz.

File metadata

  • Download URL: lp2-0.1.2.tar.gz
  • Upload date:
  • Size: 39.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for lp2-0.1.2.tar.gz
Algorithm Hash digest
SHA256 176073709d839f37f8ecf8ee86daa62379aaf25c1ca68b51fba0806df5ccb34e
MD5 d943207017a2fb48a2c416c304752c26
BLAKE2b-256 94222508c53fb4fb8c97310756126ec701bcdc43c2c28dd97ab76f4e2eb5034e

See more details on using hashes here.

Provenance

The following attestation bundles were made for lp2-0.1.2.tar.gz:

Publisher: pypi-publish.yml on daedalus/lp2

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file lp2-0.1.2-py3-none-any.whl.

File metadata

  • Download URL: lp2-0.1.2-py3-none-any.whl
  • Upload date:
  • Size: 44.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for lp2-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 26e9ce3bbc6311d7b2959b7f28d63fe7c9fe2301a7c9b14910940dbea685971e
MD5 da2370f66f14a30cc4fa1573f88e91c1
BLAKE2b-256 57b475804b0a067ba9c6e4b88ec405d19d47d18c7e662ce4464a10a3eae60296

See more details on using hashes here.

Provenance

The following attestation bundles were made for lp2-0.1.2-py3-none-any.whl:

Publisher: pypi-publish.yml on daedalus/lp2

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

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