Lean development is organized around projects. A project defines a closed environment: it fixes the Lean version, declares dependencies, and specifies how code is compiled. The structure is simple, but small details determine whether imports resolve, builds succeed, and tooling behaves predictably.
Problem
You want a project layout that supports reproducible builds, clear module boundaries, and predictable imports.
Solution
Create and work inside a Lake project. The default layout produced by:
lake new myprojhas the following structure:
myproj/
├── lakefile.lean
├── lean-toolchain
├── Myproj/
│ └── Basic.lean
├── Main.lean
└── lake-manifest.jsonEach file has a precise role.
File Roles
| File | Purpose |
|---|---|
lean-toolchain | Pins the Lean version |
lakefile.lean | Declares packages, libraries, executables |
lake-manifest.json | Resolved dependency graph |
Myproj/ | Library source directory |
Main.lean | Entry point for executables |
The directory Myproj/ corresponds to a namespace. The file:
-- Myproj/Basic.lean
namespace Myproj
def hello : String :=
"hello"
end Myprojis imported using:
import Myproj.BasicThe mapping between file paths and module names is strict. A mismatch between directory names and namespaces leads to unresolved imports.
Lake Configuration
The file lakefile.lean defines the build graph. A minimal configuration looks like:
import Lake
open Lake DSL
package «myproj» where
lean_lib Myproj
@[default_target]
lean_exe myproj where
root := `MainThis declares:
- a package named
myproj - a library
Myproj - an executable whose root module is
Main
The executable is built and run with:
lake build
lake exe myprojModule System
Lean uses a hierarchical module system. A file path:
Myproj/Data/List.leancorresponds to module:
import Myproj.Data.ListWithin the file, you typically declare:
namespace Myproj.DataNamespaces do not have to match directories exactly, but keeping them aligned avoids ambiguity and reduces the need for qualification.
Imports and Visibility
Imports are explicit. A file only sees definitions from modules it imports.
import Myproj.Basicbrings all public declarations from Myproj.Basic into scope.
You can control visibility:
open Myprojto avoid prefixing names.
Or keep names qualified:
Myproj.helloThis choice affects readability and conflict resolution in larger projects.
Dependency Management
Dependencies are declared in lakefile.lean. For example, adding mathlib:
require mathlib from git
"https://github.com/leanprover-community/mathlib4"Then update:
lake update
lake buildThe resolved versions are stored in lake-manifest.json. This file should be committed for reproducibility.
Build Model
Lake compiles modules incrementally. A change in one file triggers recompilation of dependent modules only.
Key properties:
- compilation is deterministic under fixed toolchain and dependencies
- imports define the dependency graph
- build artifacts are cached
This model supports large developments without full rebuilds.
Minimal Working Example
Library file:
-- Myproj/Math.lean
namespace Myproj
def double (n : Nat) : Nat :=
n + n
end MyprojExecutable:
-- Main.lean
import Myproj.Math
def main : IO Unit := do
IO.println (toString (Myproj.double 10))Run:
lake exe myprojExpected output:
20Common Pitfalls
If import Myproj.X fails, check that the file path is Myproj/X.lean.
If the editor shows unresolved imports, ensure you opened the project root, not a subdirectory.
If builds use the wrong Lean version, inspect lean-toolchain.
If dependencies fail to fetch, run lake update explicitly.
If names collide, avoid open and use qualified names.
Takeaway
A Lean project is a controlled environment defined by lean-toolchain and lakefile.lean. Files map directly to modules, and imports define all visibility. Keeping directory structure, module names, and namespaces aligned removes a large class of errors and makes larger systems manageable.