Getting Started
lean-py bridges Lean 4 and Python so you can call Lean functions from Python (and vice versa) with automatic type marshalling. This page walks you through a minimal end-to-end example.
Prerequisites
Installation
Python side:
uv pip install "lean_py @ git+https://github.com/BasisResearch/lean.py"
Or in pyproject.toml:
[project]
dependencies = ["lean_py @ git+https://github.com/BasisResearch/lean.py"]
The Python package discovers lean.h and libleanshared from the active
Lean toolchain at import time.
Lean side:
Add to your lakefile.toml:
[[require]]
name = "LeanPy"
git = "https://github.com/BasisResearch/lean.py"
[[lean_lib]]
name = "MyLib"
moreLinkObjs = [
"LeanPy/LeanPy:static",
"LeanPy/leanPyNative:static",
"Pantograph/Pantograph:static",
]
precompileModules = true
defaultFacets = ["shared"]
# macOS only:
moreLinkArgs = ["-Wl,-headerpad_max_install_names"]
Then build:
lake build
Note
Why three static libs? LeanPy:static is the Lean module,
leanPyNative:static is the C bridge (python_bridge.c), and
Pantograph:static is the proof-assistant kernel that LeanPy.Kernel
depends on. All three must be linked into the shared library that
Python loads.
Quick Start
1. Write Lean code
-- MyLib.lean
import LeanPy
open LeanPy
@[python "add"]
def add (a b : Int) : Int := a + b
structure Point where
x : Int
y : Int
derive_python Point
@[python "origin"]
def origin (_ : Unit) : Point := { x := 0, y := 0 }
#export_python_registry "MyLib"
2. Call from Python
from lean_py import LeanLibrary
lib = LeanLibrary.from_lake("path/to/lake/project", "MyLib", build=True)
lib.add(3, 4) # 7
lib.origin(None) # Point.mk(0, 0)
lib.Point(10, 20) # Point.mk(10, 20)
from_lake finds the shared library produced by lake build. Pass
build=True to run lake build automatically.
Pattern Matching (Python 3.10+)
Lean inductive values support Python’s match/case syntax:
from lean_py import LeanLibrary
lib = LeanLibrary.from_lake("path/to/project", "MyLib", build=True)
Shape = lib.Shape
shape = lib.mkCircle(5)
match shape:
case Shape.circle(radius):
print(f"circle with radius {radius}")
case Shape.rect(w, h):
print(f"rectangle {w}x{h}")
You can also use isinstance checks:
isinstance(shape, Shape.circle) # True
isinstance(shape, Shape.rect) # False
And access fields by index:
shape._0 # 5 (first field = radius)
Calling Python from Lean
open LeanPy.Python in
@[python "numpy_dot"]
def numpyDot (xs ys : Array Int) : IO Int := do
init ()
let np <- import_ "numpy"
let dot <- np.getAttr "dot"
let a <- Py.ofList (xs.toList.map Py.ofInt)
let b <- Py.ofList (ys.toList.map Py.ofInt)
(<- dot.call #[<- a, <- b]).toInt
lib.numpy_dot([1, 2, 3], [4, 5, 6]) # 32
Kernel Facade
Drive Lean’s type-checker and tactic engine from Python:
from lean_py import LeanLibrary
from lean_py.kernel import Kernel
lib = LeanLibrary.from_lake("path/to/project", "MyLib", build=True)
k = Kernel(lib)
k.load(["Init"])
state = k.goal_create("forall n : Nat, n + 0 = n")
print(state.pretty()) # |- forall (n : Nat), n + 0 = n
result = state.try_tactic("intro n")
print(result.state.pretty()) # n : Nat |- n + 0 = n
result2 = result.state.try_tactic("simp")
print(result2.state.is_solved()) # True
Exception Handling
Errors carry type information across the boundary:
from lean_py import LeanError, LeanPyCallbackError
try:
lib.some_io_function()
except LeanPyCallbackError as e: # Python error inside Lean callback
print(e.python_type, e.python_message)
except LeanError as e: # Lean IO error
print(e.kind, e.message)
Next Steps
Setting Up a Project – detailed guide for setting up your own project
Basic Example – the smallest end-to-end example
Pantograph Kernel – using the kernel facade
Library Loading – full API reference