I/O

What Is IO?

Hello World

i
-- Every Lean program starts with main
def main : IO Unit :=
  IO.println "Hello, world!"

#eval main
i
warning: failed to query latest release, using existing version 'leanprover/lean4:v4.30.0'
Hello, world!

Doing More Than One Thing

i
-- Use `do` to run IO actions one after another
def main : IO Unit := do
  IO.println "Step 1: start"
  IO.println "Step 2: middle"
  IO.println "Step 3: done"

#eval main
i
warning: failed to query latest release, using existing version 'leanprover/lean4:v4.30.0'
Step 1: start
Step 2: middle
Step 3: done

Forgetting do

i
-- Forgetting `do` is the most common IO mistake
def main : IO Unit :=
  IO.println "Starting..."
  IO.println "Done!"

#eval main
i
warning: failed to query latest release, using existing version 'leanprover/lean4:v4.30.0'
missing_do_err.lean:4:2: error: Application type mismatch: The argument
  IO.println
has type
  ?m.3 → IO Unit
but is expected to have type
  Void IO.RealWorld
in the application
  IO.println "Starting..." IO.println
missing_do_err.lean:6:0: error: Aborting evaluation since the expression depends on the 'sorry' axiom, which can lead to runtime instability and crashes.

To attempt to evaluate anyway despite the risks, use the '#eval!' command.

Pure Values Inside do

i
-- `let x := expr` binds a pure value inside a do block
def greet (name : String) : String :=
  s!"Hello, {name}!"

def main : IO Unit := do
  let msg := greet "Lean"
  IO.println msg

#eval main
i
warning: failed to query latest release, using existing version 'leanprover/lean4:v4.30.0'
Hello, Lean!

Functions That Return IO Actions

i
-- Functions can return IO actions, not just plain values
def printTwice (msg : String) : IO Unit := do
  IO.println msg
  IO.println msg

def main : IO Unit := do
  printTwice "echo!"
  printTwice "again"

#eval main
i
warning: failed to query latest release, using existing version 'leanprover/lean4:v4.30.0'
echo!
echo!
again
again

Capturing Results from IO

i
-- Use ← to capture the result of an IO action
def makeGreeting : IO String := do
  IO.println "Building greeting..."
  return "Hello from IO!"

def main : IO Unit := do
  let msg  makeGreeting
  IO.println msg

#eval main
i
warning: failed to query latest release, using existing version 'leanprover/lean4:v4.30.0'
Building greeting...
Hello from IO!

Reading Input

i
-- IO.getStdin gives access to standard input
def main : IO Unit := do
  IO.println "Enter your name:"
  let stdin  IO.getStdin
  let name  stdin.getLine
  IO.println s!"Hello, {name.trimAscii}!"

#eval main
i
warning: failed to query latest release, using existing version 'leanprover/lean4:v4.30.0'
Enter your name:
Hello, !

Chaining IO Actions

i
-- Chain multiple ← binds in a single do block
def main : IO Unit := do
  IO.println "Enter your first name:"
  let stdin  IO.getStdin
  let first  stdin.getLine
  IO.println "Enter your last name:"
  let last  stdin.getLine
  IO.println s!"Hello, {first.trimAscii} {last.trimAscii}!"

#eval main
i
warning: failed to query latest release, using existing version 'leanprover/lean4:v4.30.0'
Enter your first name:
Enter your last name:
Hello,  !

For Loops in do Blocks

i
-- Iterate over a list in a do block with for
def main : IO Unit := do
  let fruits := ["apple", "banana", "cherry"]
  for fruit in fruits do
    IO.println fruit

#eval main
i
warning: failed to query latest release, using existing version 'leanprover/lean4:v4.30.0'
apple
banana
cherry

For Loops Elsewhere

i
-- for with let mut works in pure functions, not just IO do blocks
def countPositive (xs : List Int) : Nat :=
  let mut count := 0
  for x in xs do
    if x > 0 then count := count + 1
  count

#eval countPositive [-3, 1, -1, 4, 1, 5, -9, 2, -6]
i
5

Accumulating Results with for

i
-- Accumulate a result across loop iterations using let mut
def main : IO Unit := do
  let words := ["hello", "world", "from", "Lean"]
  let mut count := 0
  for w in words do
    if w.length > 4 then
      count := count + 1
  IO.println s!"Words longer than 4 chars: {count}"

#eval main
i
warning: failed to query latest release, using existing version 'leanprover/lean4:v4.30.0'
Words longer than 4 chars: 2

What let mut Really Does

i
-- let mut looks like mutation but compiles to state-passing under the hood
-- The compiler rewrites mutable variables into function arguments
def countEvens (xs : List Int) : IO Nat := do
  let mut count := 0
  for x in xs do
    if x % 2 == 0 then
      count := count + 1
  return count

-- The same logic as a pure function using foldl (no IO, no mutation)
def countEvensPure (xs : List Int) : Nat :=
  xs.foldl (fun acc x => if x % 2 == 0 then acc + 1 else acc) 0

#eval countEvens [1, 2, 3, 4, 5, 6]
#eval countEvensPure [1, 2, 3, 4, 5, 6]
i
warning: failed to query latest release, using existing version 'leanprover/lean4:v4.30.0'
3
3

Summary

Errors Without Exceptions

i
-- Except carries either a success value or an error message
def safeDivide (a b : Int) : Except String Int :=
  if b == 0 then Except.error "division by zero"
  else Except.ok (a / b)

#eval match safeDivide 10 2 with
  | Except.ok n    => s!"result: {n}"
  | Except.error e => s!"error: {e}"

#eval match safeDivide 10 0 with
  | Except.ok n    => s!"result: {n}"
  | Except.error e => s!"error: {e}"
i
warning: failed to query latest release, using existing version 'leanprover/lean4:v4.30.0'
"result: 5"
"error: division by zero"

I/O Can Fail

i
-- try/catch wraps IO actions that might fail
def readFileSafe (path : String) : IO (Option String) := do
  try
    let content  IO.FS.readFile path
    return some content
  catch _ =>
    return none

def main : IO Unit := do
  -- hello_world.lean exists in this directory
  match  readFileSafe "hello_world.lean" with
  | some _ => IO.println "File found"
  | none   => IO.println "File not found"
  -- no_such_file.txt does not exist
  match  readFileSafe "no_such_file.txt" with
  | some _ => IO.println "Unexpected success"
  | none   => IO.println "Missing file handled gracefully"

#eval main
i
warning: failed to query latest release, using existing version 'leanprover/lean4:v4.30.0'
File found
Missing file handled gracefully

Three Ways to Do It

Lean splits failure across three mechanisms where Python uses exceptions for all of them:

  1. Use Option in pure functions when absence is normal and no explanation needed, e.g., looking up a key that may not exist.

  2. Use Except e a in pure functions when failure needs a reason, e.g., parsing a string that may be malformed.

  3. Use try/catch in IO actions when runtime failure outside your control, e.g., file not found, or a network error.

  4. Option and Except appear in function signatures, so the compiler forces callers to handle failure

  5. try/catch is only available in IO: you cannot silently ignore an IO exception
  6. When migrating from Python:
    • Replace None returns with Option
    • Replace ValueError/KeyError in pure logic with Except
    • Replace OSError/IOError with try/catch around IO actions

Exercises

Fix: Missing do

i
-- This should print two lines but has a bug. Fix it.
def main : IO Unit :=
  IO.println "Hello"
  IO.println "World"

#eval main
hint
  • Without do, Lean treats the body as a single expression
  • The second IO.println looks like an argument to the first

Fix: Wrong Left Arrow

i
-- Should print the greeting, but has a bug. Fix it.
def makeMsg : IO String := return "Hello, Lean!"

def main : IO Unit := do
  let msg := makeMsg
  IO.println msg

#eval main
hint
  • makeMsg returns IO String, not String
  • let msg := makeMsg stores the IO action itself rather than running it

Fix: Missing return

i
-- This function should return a greeting, but has a bug. Fix it.
def getGreeting : IO String := do
  let msg := "Hello!"
  msg

def main : IO Unit := do
  let g  getGreeting
  IO.println g

#eval main
hint
  • The function body is IO String but msg has type String
  • Lean needs return msg to lift the pure value into IO

Fix: Wrong Return Type

i
-- This function should print a message, but its return type is wrong. Fix it.
def announce (item : String) : IO String := do
  IO.println s!"Announcing: {item}"

def main : IO Unit := do
  announce "Lean 4"

#eval main
hint
  • announce prints something and returns nothing useful
  • Its return type should be IO Unit, not IO String

Fix: Arrow on a Pure Expression

i
-- Should print the doubled value, but has a bug. Fix it.
def double (n : Int) : Int := n * 2

def main : IO Unit := do
  let result  double 5
  IO.println s!"Result: {result}"

#eval main
hint
  • double 5 is a pure function call: it returns Int, not IO Int
  • is for IO actions; double 5 is not one

Fix: Silently Discarded IO Action

i
-- Should print both lines, but only prints one. Fix it.
def main : IO Unit := do
  IO.println "Step 1: start"
  let _ := IO.println "Step 2: done"

#eval main
hint
  • let _ := IO.println "Step 2: done" stores the IO action as a value — it never runs
  • The underscore (_) means "I don't intend to use this binding"
  • For an IO action, you want (run it) or plain IO.println (no binding at all)

Write: Two-Line Greeting

i
-- Add a second line so main prints "Hello, Lean!" then "Ready to go!"
def main : IO Unit := do
  IO.println "Hello, Lean!"

#eval main
hint
  • Add a second IO.println call inside the do block
  • The text should be "Ready to go!"
  • Both lines are plain IO.println calls with no

Write: Print in Uppercase

i
-- Fix printLoud so it prints the message in uppercase.
-- Hint: String.toUpper converts a String to all caps.
def printLoud (msg : String) : IO Unit :=
  IO.println msg

def main : IO Unit := do
  printLoud "hello"
  printLoud "world"

#eval main
hint
  • Replace IO.println msg with IO.println msg.toUpper
  • String.toUpper (or .toUpper) converts a string to all capitals
  • The body is a single expression, so no do needed

Write: Build a Message

i
-- Fix buildMsg so it returns "Message: " ++ text (using return).
def buildMsg (text : String) : IO String :=
  return text

def main : IO Unit := do
  let msg  buildMsg "ready"
  IO.println msg

#eval main
hint
  • The function should return "Message: " ++ text
  • Use return ("Message: " ++ text) to lift the pure result into IO
  • When you fix it, main should print Message: ready

Write: Use a Pure Function in IO

i
-- Fix main so it computes and prints the sum of the list (should be 15).
def addUp (xs : List Int) : Int := xs.foldl (· + ·) 0

def main : IO Unit := do
  IO.println "Sum is 0"

#eval main
hint
  • Call addUp [1, 2, 3, 4, 5] with let total := ... (plain let, not )
  • Then use total inside s!"Sum is {total}" in the IO.println call
  • The output should be Sum is 15

Write: Repeat Three Times

i
-- Fix repeat3 so it calls IO.println with msg exactly three times.
def repeat3 (msg : String) : IO Unit := do
  IO.println msg

def main : IO Unit := do
  repeat3 "again"

#eval main
hint
  • Add do to the body of repeat3 and call IO.println msg three times
  • Each call is a separate line in the do block
  • When fixed, main should print again three times

Write: Read and Greet

i
-- Fix main to read a name from stdin and print "Hello, <name>!"
-- instead of always printing "Hello, stranger!"
def main : IO Unit := do
  let stdin  IO.getStdin
  let _name  stdin.getLine
  IO.println "Hello, stranger!"

#eval main
hint
  • _name uses _ (underscore prefix) to suppress the unused-variable warning
    • Lean warns you when a variable is bound but never used
    • Prefixing with _ tells Lean "I'm keeping this binding but not using it yet"
  • Change _name to name so the binding is used
  • Replace "Hello, stranger!" with s!"Hello, {name.trimAscii}!"
    • getLine returns the line with the trailing newline
    • Use .trimAscii to remove it before printing