Skip to content

Lua File I/O Guide — Reading, Writing, and Processing Files

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about Lua File I/O Guide. We cover key concepts, practical examples, and best practices to help you master this topic.

Lua file I/O uses the io library to read and write files through two models -- the simple model with implicit file handles for quick operations, and the full model for fine-grained control over reading and writing.

What You'll Learn

  • Reading and writing text files with the simple model
  • Using the full I/O model with explicit file handles
  • Reading binary data
  • Handling errors and edge cases

Why It Matters

File I/O is essential for configuration files, data processing, logging, and persistence. Durga Antivirus Pro uses Lua's file I/O to read malware signature databases and write scan reports. Game engines use file I/O to load level data, save game progress, and parse configuration files.

Real-World Use

A log analyzer reads Apache access logs line by line and aggregates statistics. A game saves player progress and inventory to a file. A configuration system reads .ini or .lua files at startup to configure application behavior.

flowchart LR
    A["File I/O"] --> B["Simple Model"]
    A --> C["Full Model"]
    B --> D["io.open"]
    C --> D
    D --> E["Read"]
    D --> F["Write"]
    E --> G["Close"]
    A:::current --> B
    style A fill:#2563eb,stroke:#2563eb,color:#fff
    style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b

The Simple I/O Model

For quick read/write operations without managing file handles.

-- Reading entire file
local content = io.open("input.txt", "r"):read("*a")
print(content)

-- Writing to a file
local file = io.open("output.txt", "w")
file:write("Line 1\n")
file:write("Line 2\n")
file:close()

-- Appending to a file
local file = io.open("log.txt", "a")
file:write("New log entry\n")
file:close()

The Full I/O Model

More control with explicit file handles.

-- Opening a file for reading
local file, err = io.open("data.txt", "r")
if not file then
    print("Error opening file: " .. err)
    return
end

-- Reading modes
-- file:read("*a")  -- read entire file
-- file:read("*l")  -- read next line (default)
-- file:read("*n")  -- read a number
-- file:read(n)     -- read n bytes

-- Read line by line
local line = file:read()
while line do
    print(line)
    line = file:read()
end

-- Always close
file:close()

Read Modes

local file = io.open("data.txt", "r")

-- Read entire file
local all = file:read("*a")
print("File length: " .. #all)

file:seek("set", 0)  -- rewind to beginning

-- Read as number
local num = file:read("*n")
print("First number: " .. num)

file:seek("set", 0)

-- Read specific bytes
local bytes = file:read(10)
print("First 10 bytes: " .. bytes)

file:close()

Write Modes and Buffering

local file = io.open("output.txt", "w")

file:write("First line\n")
file:write("Second line\n")
file:write("Third line\n")

-- Flush buffer explicitly (auto-flushed on close)
file:flush()

-- Set output buffer
file:setvbuf("line")  -- line buffering
file:setvbuf("no")    -- no buffering
file:setvbuf("full", 4096)  -- full buffering with 4KB buffer

file:close()

Looping with file:lines

-- Most idiomatic way to read a file line by line
for line in io.lines("data.txt") do
    print(line)
end

-- Equivalent with explicit handle
local file = io.open("data.txt", "r")
for line in file:lines() do
    print(line)
end
file:close()

File Positioning

local file = io.open("data.txt", "r")

-- Get current position
local pos = file:seek()
print("Current position: " .. pos)

-- Read some data
local data = file:read(5)
print("Read: " .. data)

-- Seek relative to current position
file:seek("cur", 2)  -- skip 2 bytes forward

-- Seek relative to end (negative offset)
local file_size = file:seek("end", 0)
print("File size: " .. file_size)

-- Seek absolute from beginning
file:seek("set", 0)  -- rewind to start

file:close()

Binary File I/O

-- Reading binary data
local file = io.open("image.png", "rb")
-- "b" flag for binary mode (on Windows, prevents \n conversion)

local header = file:read(8)  -- PNG signature
local _, _, width, height = string.unpack(
    ">I4I4",
    file:read(8)
)
print("Width: " .. width .. ", Height: " .. height)

file:close()

-- Writing binary data
local out = io.open("output.bin", "wb")
local data = string.pack(">I4I4", 1920, 1080)
out:write(data)
out:close()

Reading CSV Files

local function parse_csv_line(line)
    local fields = {}
    local i = 1
    while i <= #line do
        if line:sub(i, i) == '"' then
            -- Quoted field
            i = i + 1
            local start = i
            while i <= #line do
                if line:sub(i, i) == '"' then
                    if line:sub(i + 1, i + 1) == '"' then
                        i = i + 2
                    else
                        break
                    end
                else
                    i = i + 1
                end
            end
            table.insert(fields, line:sub(start, i - 1))
            i = i + 2  -- skip closing quote and comma
        else
            -- Unquoted field
            local comma = line:find(",", i)
            if comma then
                table.insert(fields, line:sub(i, comma - 1))
                i = comma + 1
            else
                table.insert(fields, line:sub(i))
                break
            end
        end
    end
    return fields
end

for line in io.lines("data.csv") do
    local fields = parse_csv_line(line)
    for j, field in ipairs(fields) do
        print("Field " .. j .. ": " .. field)
    end
end

Error Handling

-- Always check io.open return value
local file, err = io.open("nonexistent.txt", "r")
if not file then
    print("Error: " .. err)
    return
end

-- Check write operations
local success, err = file:write("data")
if not success then
    print("Write error: " .. err)
end

-- Safely close with error handling
local success, err = file:close()
if not success then
    print("Close error: " .. err)
end

Common Mistakes

1. Not closing files

Open files consume system resources. Always close files with file:close() or use io.lines which handles closing automatically.

2. Not checking io.open return value

io.open returns nil, error_message on failure. Without checking, the nil file handle causes "attempt to index a nil value" errors.

3. Confusing read modes

read("*a") reads all, read("*l") reads a line, read(5) reads 5 bytes. Mixing them up leads to wrong data.

4. Forgetting binary mode on Windows

On Windows, "r" mode converts \n to \r\n. Use "rb" for binary files to prevent data corruption.

5. Not setting file position after read

After reading part of a file, the position advances. If you need to re-read from the beginning, use file:seek("set", 0).

Practice Questions

1. What does io.open("file.txt", "r") return on failure? It returns nil and an error message. Always check the first return value before using the file handle.

2. How do you read an entire file as a single string? Use file:read("*a") after opening the file. Or io.open("file", "r"):read("*a") for simple cases.

3. What is the difference between io.lines and file:lines? io.lines opens, reads, and closes the file automatically. file:lines requires an already-opened file handle.

Challenge: Write a function that reads a file, counts the number of lines, words, and characters, and returns them as a table.

FAQ

{{< faq question="What file modes does Lua support?" >}} "r" read, "w" write (overwrites), "a" append, "r+" read/write, "w+" read/write (overwrites), "a+" read/append. Add "b" for binary mode on Windows. {{< /faq >}}

{{< faq question="How do I check if a file exists?" >}} Try to open it with io.open(file, "r"). If it returns nil, the file doesn't exist or can't be read. There's no dedicated file-exists function in standard Lua. {{< /faq >}}

{{< faq question="Can I use Lua for binary file processing?" >}} Yes. Use "rb" mode and string.pack/string.unpack for structured binary data. The string library provides byte-level operations. {{< /faq >}}

{{< faq question="What is the maximum file size Lua can handle?" >}} Lua can handle files up to the available memory limit, since read("*a") loads the entire file into memory. For larger files, read in chunks: file:read(4096) in a loop. {{< /faq >}}

{{< faq question="Are file operations thread-safe in Lua?" >}} Standard Lua is single-threaded within a Coroutine/thread, so file operations within one Lua state are safe. Across multiple Lua states, use OS-level synchronization. {{< /faq >}}

Try It Yourself

-- Word count tool
local function word_count(filename)
    local file = io.open(filename, "r")
    if not file then
        return nil, "Cannot open " .. filename
    end

    local lines = 0
    local words = 0
    local chars = 0

    for line in file:lines() do
        lines = lines + 1
        chars = chars + #line + 1  -- +1 for newline
        for _ in line:gmatch("%S+") do
            words = words + 1
        end
    end

    file:close()
    return {lines = lines, words = words, chars = chars}
end

-- Test on this script
local stats = word_count("input.txt")
if stats then
    print("Lines: " .. stats.lines)
    print("Words: " .. stats.words)
    print("Chars: " .. stats.chars)
end

What's Next

Now that you understand file I/O, learn about modules and how to organize Lua code into reusable components.

Topic Description Link
Modules require, module patterns, packages {{< ref "10-modules" >}}
C API Embedding and extending Lua in C {{< ref "11-c-api" >}}
Python I/O Compare with Python file handling Python

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro