Standard Library: io, os and math
Reviewed & published by Brayan K
By the end of this lesson you'll use three workhorse standard libraries: io for reading and writing files and the console, os for time, dates, environment variables, and processes, and math for rounding, randomness, constants, and numeric helpers — the everyday toolkit behind real Lua scripts.
Part of the free Lua course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn
- Open, read, write, and close files with io.open and handles
- Read whole files, line by line, and from standard input
- Get and format time and dates with os.time and os.date
- Measure CPU time, read env vars, and run commands with os
- Round, take roots, and find max/min with the math library
- Generate seeded random numbers with math.random and randomseed
💡 Real-World Analogy
The standard library is your program's workshop drawers. The io drawer holds the tools for moving things in and out — pens for writing files, a reader for taking them back in. The os drawer is the clock-and-calendar plus a phone line to the operating system: what time is it, what's in the environment, run this command. The math drawer is the calculator and dice cup. You don't build these tools yourself; you just know which drawer to open, and your scripts suddenly handle files, time, and numbers like a pro.
1. io: Files & the Console
The io library handles input and output. io.open(name, mode) returns a file handle (or nil plus a message on failure) — modes are "r" read, "w" write (which truncates!), and "a" append. With a handle you :write(...), :read("a") for the whole file, or loop over :lines() for memory-friendly line-by-line reading, and you must :close() when done. io.write prints to stdout without formatting, and io.read reads from stdin. Read and run this.
-- The 'io' library reads and writes files and the console.
-- io.open(name, mode) returns a file HANDLE, or nil + an error message.
-- "r" read (default) "w" write (truncates!) "a" append
-- add "b" for binary, e.g. "rb"
-- WRITE a file, then ALWAYS close it.
local out, err = io.open("notes.txt", "w")
if not out then error(err) end -- nil-plus-message convention
out:write("line one\n")
out:write("line two\n")
out:close()
-- READ it back in different ways.
local f = io.open("notes.txt", "r")
print(f:read("a")) -- "a" (or "*a") reads the WHOLE file as one string
f:close()
-- Read LINE BY LINE with the lines() iterator (memory-friendly).
for line in io.lines("notes.txt") do
print("> " .. line)
end
-- io.write goes to standard output (no newline added, no print formatting).
io.write("no newline here")
io.write(" -- still same line\n")
-- io.read reads from standard input (stdin):
-- local name = io.read("l") -- read one line the user types2. os: Time, Dates, Environment & Processes
The os library is your link to the operating system. os.time() returns the current time as a number of seconds, which os.date(fmt) formats with strftime codes like %Y-%m-%d. os.clock() measures CPU time — ideal for benchmarking a block of code. os.getenv(name) reads an environment variable, os.execute(cmd) runs a shell command, and os.exit(code) ends the program with a status. Don't confuse os.time (wall-clock) with os.clock (CPU time).
-- The 'os' library exposes the operating system: time, env, processes.
-- TIME as a number (seconds since an epoch) and a formatted date string.
local now = os.time()
print(type(now)) -- prints: number
print(os.date("%Y-%m-%d", now)) -- prints e.g. 2026-06-21 (strftime codes)
print(os.date("%H:%M:%S")) -- current time of day
-- os.clock measures CPU time used — great for timing a block of code.
local start = os.clock()
local sum = 0
for i = 1, 1000000 do sum = sum + i end
print("took", os.clock() - start, "cpu seconds")
-- ENVIRONMENT variables and running external commands.
print(os.getenv("HOME")) -- the HOME env var (or nil if unset)
-- os.execute("echo hello") -- runs a shell command (returns status)
-- os.exit ends the program immediately with an optional status code.
-- os.exit(0) -- 0 (or true) means success; nonzero/false means failureYour turn. Write two scores to a file, then read them back line by line. Fill in the blanks marked ___.
-- 🎯 YOUR TURN — write a file, then read every line back.
-- 1) Open scores.txt for WRITING (truncating mode)
local out = io.open("scores.txt", "___") -- 👉 the write mode letter
out:write("10\n")
out:write("20\n")
out:close()
-- 2) Read it back line by line and print each one
for line in io.lines("scores.txt") do
print(___) -- 👉 print the current line
end
-- ✅ Expected output:
-- 10
-- 203. math: Rounding, Randomness & Constants
The math library covers numbers. Round with floor (down) and ceil (up). For randomness, seed once with math.randomseed(os.time()) so each run differs, then draw with math.random() (a float in [0,1)), math.random(n) (an integer 1..n), or math.random(a, b) (an integer a..b). Reach for constants and helpers like math.pi, math.huge (infinity), math.sqrt, math.max/math.min, math.abs, and math.fmod. Read and run this.
-- The 'math' library: rounding, randomness, constants, and helpers.
-- ROUNDING: floor rounds down, ceil rounds up (both return integers).
print(math.floor(3.7)) -- prints: 3
print(math.ceil(3.2)) -- prints: 4
-- RANDOM: seed once (so each run differs), then draw numbers.
math.randomseed(os.time()) -- vary the sequence between runs
print(math.random()) -- a float in [0, 1)
print(math.random(6)) -- an integer 1..6 (a dice roll)
print(math.random(10, 20)) -- an integer 10..20 inclusive
-- CONSTANTS and common helpers.
print(math.pi) -- 3.1415926535898
print(math.huge) -- inf (positive infinity)
print(math.sqrt(144)) -- 12.0
print(math.max(3, 9, 2)) -- 9
print(math.min(3, 9, 2)) -- 2
print(math.abs(-7)) -- 7
print(math.fmod(10, 3)) -- 1.0 (floating-point remainder)Now you try combining os and math. Seed the generator, roll a die, and print today's date. Fill in the blanks:
-- 🎯 YOUR TURN — seed randomness and roll a die.
-- 1) Seed the generator with the current time so each run differs
math.randomseed(os.___()) -- 👉 the os function that returns seconds
-- 2) Roll a six-sided die: a random integer from 1 to 6
local roll = math.random(___) -- 👉 the highest face value
print("you rolled", roll)
-- 3) Print today's date as YYYY-MM-DD
print(os.date("___")) -- 👉 the strftime format "%Y-%m-%d"
-- ✅ Example output (values vary):
-- you rolled 4
-- 2026-06-21Pro Tips
- 💡 Always close handles. Call file:close() when done; unclosed handles can lose buffered writes or hit OS limits.
- 💡 Check io.open. It returns nil plus a message on failure, so guard with if not f then ... end before using the handle.
- 💡 Seed randomness once. Call math.randomseed(os.time()) near startup, not before every math.random.
- 💡 os.time vs os.clock. Use os.time for dates and timestamps, os.clock only for measuring CPU time.
Common Errors (and the fix)
- "attempt to index a nil value (handle)": io.open failed and you used the nil handle. Fix: check local f, err = io.open(...) and handle the nil case first.
- Overwriting a file by accident: opening in "w" truncates it instantly. Fix: use "a" to append, or "r" when you only mean to read.
- Same random numbers every run: you forgot math.randomseed. Fix: seed once with math.randomseed(os.time()) at startup.
- Confusing os.time and os.clock: timing a block with os.time gives whole-second resolution at best. Fix: use os.clock() deltas for measuring code speed.
📋 Quick Reference
| Task | Code | Result / Note |
|---|---|---|
| Open file | io.open("f.txt", "r") | handle or nil,err |
| Read all / lines | f:read("a") / io.lines(p) | string / iterator |
| Write / close | f:write(s); f:close() | always close |
| Time / date | os.time() / os.date("%Y") | seconds / formatted |
| CPU time / env | os.clock() / os.getenv("X") | benchmark / env var |
| Round / sqrt | math.floor(x) / math.sqrt(x) | down / root |
| Random / consts | math.random(1,6) / math.pi | seed first |
Mini-Challenge: a line-counting tool
No blanks this time — just a brief and an outline. You'll combine io.open (guarding its nil-plus-message return), a lines() loop, and optionally os.clock() to time it. Build it, run it, and check your output.
-- 🎯 MINI-CHALLENGE: a tiny line-counting tool
-- 1. Write countLines(path) that opens the file for reading with io.open.
-- Remember io.open returns nil + message on failure, so guard it
-- (return nil, err) instead of crashing.
-- 2. Use a for loop over file:lines() (or io.lines) to count the lines,
-- incrementing a counter each iteration.
-- 3. Close the handle and return the count.
-- 4. Bonus: time the whole thing with os.clock() and print the duration.
--
-- ✅ Example shape:
-- local n = countLines("notes.txt")
-- print(n .. " lines")
-- your code here🎉 Lesson Complete!
- ✅ io.open returns a handle (or nil + message); read with read("a") or lines(), and always close()
- ✅ io.write prints to stdout, io.read reads from stdin
- ✅ os.time/os.date give timestamps and formatted dates; os.clock measures CPU time
- ✅ os.getenv, os.execute, and os.exit reach into the environment and processes
- ✅ math rounds, takes roots, finds max/min, and generates seeded random numbers; math.huge is infinity
- ✅ Course complete! You've finished the Lua track — revisit any lesson to lock it in
Practice quiz
Which function opens a file and returns a file handle?
- io.open
- io.file
- io.load
- io.read
Answer: io.open. io.open(name, mode) returns a file handle on success, or nil plus an error message on failure.
What does io.open return when it fails to open a file?
- It raises an error
- An empty handle
- nil and an error message
- false only
Answer: nil and an error message. io.open follows the nil-plus-message convention so you can check before using the handle.
Which mode string opens a file for writing, truncating existing contents?
- "a"
- "w"
- "x"
- "r"
Answer: "w". Mode 'w' opens for writing and discards any existing contents; 'a' appends, 'r' reads.
Which method reads a file one line at a time in a for loop?
- file:all()
- file:scan()
- file:next()
- file:lines()
Answer: file:lines(). file:lines() returns an iterator yielding each line, perfect for a for loop.
How do you read a whole line from standard input?
- io.read()
- io.scan()
- io.get()
- io.input()
Answer: io.read(). io.read() (defaulting to a line read) returns the next line typed on stdin.
Which os function returns the current time as a number of seconds?
- os.clock
- os.time
- os.tick
- os.now
Answer: os.time. os.time() returns the current time as a numeric timestamp (seconds since an epoch).
What does os.clock measure?
- The system uptime
- The file modification time
- Wall-clock date
- CPU time used by the program
Answer: CPU time used by the program. os.clock returns an approximation of CPU time used, useful for benchmarking code.
Which math function returns a pseudo-random number?
- math.dice
- math.rand
- math.random
- math.rnd
Answer: math.random. math.random() gives a float in [0,1); with arguments it returns an integer in a range.
Why call math.randomseed before using math.random?
- It is required or random errors
- To get a different sequence each run instead of a fixed one
- To make numbers larger
- To reset the io library
Answer: To get a different sequence each run instead of a fixed one. Seeding (often with os.time()) varies the sequence between runs; without it you may get the same numbers each time.
Which math value represents positive infinity?
- math.huge
- math.pi
- math.max
- math.inf
Answer: math.huge. math.huge is the representation of infinity; math.pi is the constant pi, not infinity.
Continue this course
- Previous: Error Handling: pcall & error
Frequently asked questions
How do I read an entire file versus reading it line by line?
Open the file with io.open(path, 'r'), which gives you a handle. To slurp the whole file into one string call handle:read('a') (the 'a' format means 'all'). To process it without loading everything into memory, loop over an iterator instead: for line in io.lines(path) do ... end, or handle:lines() on an open handle, yields one line per iteration. Line-by-line is the better choice for large files because it keeps memory use flat, while read('a') is convenient for small files you want as a single string.
What do the io.open mode strings mean?
The mode is a short string. 'r' opens for reading and is the default. 'w' opens for writing but truncates the file, wiping any existing contents, so be careful. 'a' opens for appending, adding to the end without erasing what is there. There are read/write variants like 'r+' and 'w+', and you append 'b' for binary mode (for example 'rb') on systems that distinguish it. io.open returns the handle on success or nil plus an error message on failure, so always check before using the handle, and call handle:close() when done.
What is the difference between os.time and os.clock?
os.time returns the current wall-clock time as a numeric timestamp, the number of seconds since the system epoch, which you pass to os.date to format as a human-readable date. os.clock is completely different: it returns an approximation of the CPU time your program has consumed, not the time of day. Use os.time for timestamps and dates, and os.clock for benchmarking, by recording os.clock() before and after a block and subtracting to get the CPU seconds it took.
Why must I call math.randomseed, and what should I seed with?
math.random produces a deterministic pseudo-random sequence, so without seeding you can get the same numbers every time the program runs. Calling math.randomseed sets the starting point of that sequence; seeding with a value that changes between runs, most commonly os.time(), gives you a different sequence each run. You only need to seed once, typically near the start of your program. After seeding, math.random() returns a float in the range zero up to but not including one, math.random(n) an integer from 1 to n, and math.random(a, b) an integer from a to b inclusive.
What are math.huge and math.pi for?
math.pi is the mathematical constant pi (about 3.14159), handy for geometry and trigonometry. math.huge represents positive infinity, larger than any finite number, and negating it gives negative infinity. math.huge is useful as a starting value when searching for a minimum or maximum: initialise your 'smallest so far' to math.huge so the first real value is always smaller, or to -math.huge when hunting for a maximum. It also appears as the result of operations that overflow the floating-point range.