Modules & Imports
Organize code into modules for reusability and maintainability. Control what's exported and how it's imported.
File = Module
Each .lean file is a module. The file path determines the module name:
1MyProject/2├── MyProject.lean -- Module: MyProject3├── MyProject/4│ ├── Utils.lean -- Module: MyProject.Utils5│ ├── Utils/6│ │ └── String.lean -- Module: MyProject.Utils.String7│ └── Core.lean -- Module: MyProject.CoreImporting Modules
The importstatement makes another module's definitions available. All public definitions from the imported module become accessible.
1-- Import entire module2import MyProject.Utils34-- Import brings all public definitions into scope5-- You can now use: Utils.helper, Utils.Config, etc.67-- Import multiple modules8import MyProject.Utils9import MyProject.CoreSelective Imports
You can import a module but open only specific names to keep your namespace clean.
1import MyProject.Utils23-- Open only a few definitions4open MyProject.Utils (Config defaultConfig)56-- Other names remain qualified7-- #check helper -- not in scope unless openedNamespaces
Namespaces group related definitions under a common prefix. This prevents name collisions and makes code organization explicit.
1namespace Math23def square (x : Nat) : Nat := x * x4def cube (x : Nat) : Nat := x * x * x56end Math78-- Use with qualified name9#eval Math.square 5 -- 2510#eval Math.cube 3 -- 27Opening Namespaces
Opening a namespace brings its definitions into scope without requiring the full prefix. You can open temporarily for one expression or for the rest of the file.
1-- Open brings names into scope2open Math34#eval square 5 -- No Math. prefix needed56-- Open temporarily in expression7#eval (open Math in square 5)89-- Open specific names only10open Math (square)11#eval square 5 -- Works12-- #eval cube 3 -- Error: not in scopeSections
Sections scope variables and attributes:
1section VectorOps2 variable (n : Nat) -- Available in this section34 def zeros : List Nat := List.replicate n 05 def ones : List Nat := List.replicate n 167end VectorOps89-- n is no longer in scope here10#eval zeros 5 -- [0, 0, 0, 0, 0]Export and Visibility
Control what's visible outside your module:
1-- Private: only visible in this file2private def helper (x : Nat) : Nat := x + 134-- Protected: visible with namespace prefix5protected def Config.default : Config := ⟨0, ""⟩67-- Public (default): visible everywhere8def publicFunc : Nat := 42Re-exporting
1-- In MyProject/Prelude.lean2import MyProject.Utils3import MyProject.Core45-- Re-export so users only need one import6export MyProject.Utils (helper Config)7export MyProject.Core (main)1-- Users just import Prelude2import MyProject.Prelude34-- Gets helper, Config, and mainModule Initialization
initialize registers an IO action to run once, when the module is first loaded — before any code that imports it. This is how libraries register attributes, tactics, and global state.
1-- Run an action at load time. Note: no ":" and no type after initialize.2initialize do3 IO.println "Initializing MyModule..."45-- Bind the result to a name with ← : the classic use is a global reference6initialize counterRef : IO.Ref Nat ← IO.mkRef 078-- Register an environment extension (how attributes are implemented)9initialize myExtension : EnvExtension Nat ←10 registerEnvExtension (pure 0)initialize do … — writing initialize : IO Unit := do is a parse error. Second, a name introduced by initialize cannot be #eval'd in the same file: Lean reports cannot evaluate [init] declaration in the same module, because the initializer has not run yet while that file is being elaborated. Use it from an importing module.#check and #eval do not run on import. They run once, when their own file is elaborated, and leave nothing behind but their output in the infoview. Only initialize gives you load-time behaviour.Mutual Dependencies
Lean doesn't allow circular imports. Structure your code to avoid them:
1-- Bad: A imports B, B imports A2-- This won't compile34-- Good: extract shared code to a third module5-- Common.lean - shared definitions6-- A.lean - imports Common7-- B.lean - imports CommonDeep Dive: Prelude and Init
Every Lean file implicitly imports the Prelude, which provides basic types and functions:
1-- These are available without import:2-- Nat, Int, Bool, String, List, Option, etc.34-- To write a file without Prelude:5prelude -- Must be first lineThe prelude directive is used when defining the Prelude itself or for specialized low-level code.
Common Patterns
Barrel File
1-- MyProject.lean (root module)2-- One import for users, instead of five34import MyProject.Types5import MyProject.Utils6import MyProject.Core78-- Note: "export Ns" alone is a parse error. export always takes a9-- parenthesised list of names.10export MyProject.Types (Config Status)11export MyProject.Utils (helper format)12export MyProject.Core (run)Internal Module
1-- MyProject/Internal.lean2-- Implementation details not for public use34private def internalHelper (n : Nat) : Nat := n * 256private structure InternalState where7 cache : List Nat8 dirty : Bool910-- private means "not visible outside THIS FILE" - not "outside this11-- namespace". An importing module cannot see either of the above,12-- even fully qualified.Feature Modules
1-- Each feature in its own namespace2-- MyProject/Feature/Auth.lean3namespace MyProject.Feature.Auth4 def login (user : String) : Bool := user != ""5 def logout : Unit := ()6end MyProject.Feature.Auth78-- MyProject/Feature/Data.lean9namespace MyProject.Feature.Data10 def load (path : String) : IO String := IO.FS.readFile path11 def save (path content : String) : IO Unit := IO.FS.writeFile path content12end MyProject.Feature.DataBest Practices
- One concept per module: Keep modules focused
- Match file and namespace:
MyProject/Utils.leandefinesMyProject.Utils - Use private for internals:Don't expose implementation details
- Provide a root module: Re-export the public API
- Avoid deep nesting: 3-4 levels maximum
Define two functions in a namespace and call them with and without open.
1namespace Strings2def shout (s : String) : String := s.toUpper3def whisper (s : String) : String := s.toLower4end Strings56#eval Strings.shout "Lean"78open Strings9#eval whisper "LEAN"