Reading and Writing Files in Lua

· 7 min read · Updated March 17, 2026 · beginner
file-io io stdlib tutorial

Introduction

File I/O is a fundamental skill for any programmer, and Lua provides a clean and straightforward way to work with files through its io library. Whether you’re building a configuration system, processing data files, or logging application events, understanding how to read from and write to files is essential.

In this tutorial, we’ll cover everything you need to know about file operations in Lua 5.4, from opening and closing files to reading and writing data using various methods. We’ll also discuss best practices to ensure your file handling code is robust and efficient.

Opening and Closing Files

The first step in any file operation is opening the file. In Lua, you use the io.open() function to open a file and obtain a file handle. This handle is your gateway to reading from or writing to the file.

-- Open a file for reading
local file = io.open("myfile.txt", "r")

-- Check if the file was opened successfully
if file then
    -- Work with the file
    file:close()
else
    print("Failed to open file")
end

The io.open() function takes two arguments: the file path and the mode string. The mode determines what operations you can perform on the file.

File Modes

Lua supports several file modes that control how a file is opened:

ModeDescription
"r"Read mode (default) - Opens for reading from the beginning
"w"Write mode - Opens for writing, creates new or truncates existing
"a"Append mode - Opens for writing, creates new or appends to end
"r+"Read/Write - Opens for both reading and writing, starting at beginning
"w+"Read/Write - Creates new or truncates existing, allows both operations
"a+"Append Read/Write - Creates new or appends, allows both operations

You can also add "b" to the mode string to open the file in binary mode, which prevents Lua from interpreting the file contents as text:

-- Open a binary file
local binary_file = io.open("data.bin", "rb")

Reading from Files

Lua’s io library provides several methods for reading file contents. The right method depends on your specific needs.

Reading with file:read()

The read() method reads data according to format specifiers. Here are the most common ones:

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

if file then
    -- Read a single line
    local line = file:read("*l")
    print(line)
    
    -- Read all remaining lines into a table
    local all_lines = file:read("*a")
    print(all_lines)
    
    -- Read a specific number of bytes
    local bytes = file:read(10)  -- Read 10 bytes
    
    file:close()
end

The format specifiers for read() include:

  • "*l" or "l": Reads the next line (excluding the newline character)
  • "*L" or "L": Reads the next line (including the newline character)
  • "*a" or "a": Reads the entire file
  • "*n" or "n": Reads a number
  • A number: Reads that many bytes

Reading All Lines with file:lines()

If you need to iterate through all lines in a file, the lines() method provides an elegant solution:

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

if file then
    for line in file:lines() do
        print(line)
    end
    file:close()
end

The lines() method returns an iterator function that reads one line at a time. This is memory-efficient for large files since you don’t load the entire file into memory at once.

Reading the Entire File at Once

For smaller files, reading everything at once can be convenient:

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

if file then
    local content = file:read("*a")
    file:close()
    print(content)
end

Using "*a" reads the entire file content into a single string. However, be cautious with large files as this approach consumes memory proportional to file size.

Writing to Files

Writing files follows a similar pattern to reading. You open the file in a write-capable mode and use the write() method.

Basic Writing with file:write()

-- Open file for writing (creates new or truncates existing)
local file = io.open("output.txt", "w")

if file then
    file:write("Hello, World!\n")
    file:write("This is line two.\n")
    file:write("Number: ", 42, "\n")
    file:close()
else
    print("Could not open file for writing")
end

The write() method accepts multiple arguments and converts them to strings automatically. It doesn’t add newlines automatically, so you must include "\n" when you want line breaks.

Appending to Files

To add content to the end of an existing file without overwriting it, use append mode:

-- Open file for appending
local file = io.open("log.txt", "a")

if file then
    local timestamp = os.date("%Y-%m-%d %H:%M:%S")
    file:write(string.format("[%s] Application started\n", timestamp))
    file:close()
end

This is particularly useful for logging applications where you want to maintain a chronological record of events.

Using the Global io.write()

Lua also provides a global write() function that writes to the standard output. However, for file operations, always use the file handle method to ensure you’re writing to the correct destination.

Best Practices

Following best practices ensures your file handling code is reliable and maintainable.

Always Close Files

Always close files after you’re done with them. Leaving files open can lead to resource leaks and data loss if the program crashes before the file is properly flushed:

local file = io.open("data.txt", "r")
if file then
    local content = file:read("*a")
    file:close()
    -- Process content
end

Use Protected Mode with xpcall

For robust error handling, wrap file operations in protected calls:

local function read_file_safely(filename)
    local file, err = io.open(filename, "r")
    if not file then
        return nil, err
    end
    
    local success, content = pcall(function()
        return file:read("*a")
    end)
    
    file:close()
    
    if success then
        return content
    else
        return nil, content
    end
end

local content, err = read_file_safely("important.txt")
if not content then
    print("Error reading file:", err)
end

Check File Existence Before Opening

Before attempting to open a file, you might want to verify it exists:

local function file_exists(filename)
    local file = io.open(filename, "r")
    if file then
        file:close()
        return true
    end
    return false
end

if file_exists("config.txt") then
    -- Proceed with reading
end

Use io.input() and io.output() for Default Streams

Lua allows you to set default input and output streams, which can simplify code when working with a single file:

-- Set default input file
io.input("input.txt")

-- Now read from the default input
local content = io.read("*a")

-- Close the default input
io.input():close()

-- Set default output file
io.output("output.txt")

-- Write to the default output
io.write("Some data\n")

-- Close the default output
io.output():close()

This approach can make code more readable when you’re working with one file at a time, but be careful as it modifies global state.

Working with Paths

When working with files, you’ll often need to handle file paths. Lua doesn’t have a built-in path library in its standard distribution, but you can use string manipulation:

local function get_extension(filename)
    return filename:match("%.([^%.]+)$")
end

local function get_basename(filepath)
    return filepath:match("([^/]+)$")
end

print(get_extension("document.txt"))  -- "txt"
print(get_basename("/path/to/file.txt"))  -- "file.txt"

For more complex path operations, consider using Lua libraries like luafilesystem (lf), which provides portable file system operations.

Example: Processing a Data File

Let’s put everything together with a practical example that processes a simple data file:

-- Data file format: name,age,city (one per line)
-- Example:
-- Alice,30,London
-- Bob,25,Manchester

function process_data_file(filename)
    local file = io.open(filename, "r")
    if not file then
        return nil, "Could not open file"
    end
    
    local people = {}
    
    for line in file:lines() do
        local name, age, city = line:match("([^,]+),([^,]+),(.+)")
        if name and age and city then
            table.insert(people, {
                name = name,
                age = tonumber(age),
                city = city
            })
        end
    end
    
    file:close()
    return people
end

-- Process and display results
local people, err = process_data_file("people.txt")
if people then
    for _, person in ipairs(people) do
        print(string.format("%s is %d years old and lives in %s",
            person.name, person.age, person.city))
    end
else
    print("Error:", err)
end

This example demonstrates reading line by line, parsing data, and properly closing the file.

Summary

Lua’s io library provides everything you need for file operations:

  • Opening files: Use io.open(path, mode) with modes like "r", "w", "a", and their combinations
  • Reading: Use file:read() with format specifiers or file:lines() for iteration
  • Writing: Use file:write() to write data, and use "a" mode for appending
  • Best practices: Always close files, use protected calls for error handling, and consider memory usage with large files

With these fundamentals, you can build file-based applications in Lua, from simple configuration loaders to complex data processing systems. Practice with different file modes and reading methods to become comfortable with file I/O in Lua.