Polyglot
Languages Lua modules
Lua § modules

Modules and packaging

A Lua module is a Lua source file (or a binary C library) returning a table of functions and values. The principal mechanism is require — admit loading, caching, and returning a module’s exports. The package.path and package.cpath define where modules are searched. LuaRocks is the conventional contemporary package manager for distributing Lua modules. The combination — file-as-module, require for loading with caching, the search-path mechanism, the LuaRocks ecosystem — is the substance of Lua’s package management.

A module

The conventional pattern: a module is a file returning a table:

-- File: greetings.lua
local M = {}

function M.hello(name)
    return "Hello, " .. name
end

function M.goodbye(name)
    return "Goodbye, " .. name
end

M.default_greeting = "Hi"

return M

Importing:

-- File: main.lua
local greetings = require("greetings")

print(greetings.hello("Alice"))                    -- "Hello, Alice"
print(greetings.default_greeting)                  -- "Hi"

require

The require mechanism:

  1. Checks package.loaded[name] — returns cached if present.
  2. Searches package.path for .lua files; tries package.cpath for .so/.dll.
  3. Loads and runs the file (which returns the module table).
  4. Caches in package.loaded[name].
  5. Returns the module.

The mechanism admits load-once semantics — substantial efficiency for repeated requires.

Module-private state

The conventional pattern uses local for module-private:

-- File: counter.lua
local M = {}

local count = 0                                    -- private to module

function M.increment()
    count = count + 1
    return count
end

function M.value()
    return count
end

function M.reset()
    count = 0
end

return M
local counter = require("counter")
counter.increment()                                -- 1
counter.increment()                                -- 2
print(counter.value())                             -- 2

The module’s own local variables are not visible to callers — admit substantial encapsulation.

package.path

The search path for .lua files:

print(package.path)
-- "./?.lua;./?/init.lua;/usr/local/share/lua/5.4/?.lua;..."

The ? admits substitution of the module name. For example, require("foo") searches:

  • ./foo.lua
  • ./foo/init.lua
  • /usr/local/share/lua/5.4/foo.lua

For substantial customisation:

package.path = package.path .. ";./vendor/?.lua"

package.cpath

For C modules (.so on Linux, .dll on Windows):

print(package.cpath)
-- "./?.so;/usr/local/lib/lua/5.4/?.so;..."

local mylib = require("mylib")                     -- loads mylib.so

The C modules expose a luaopen_<name> function that registers Lua bindings.

Submodules

The . admits submodule paths:

-- File: mylib/utils.lua
local M = {}

function M.format(x)
    return tostring(x)
end

return M
local utils = require("mylib.utils")
-- The . admits searching mylib/utils.lua

The require("a.b.c") searches for a/b/c.lua, a/b/c/init.lua, etc.

init.lua

A directory may admit a init.lua as the package’s main file:

mylib/
├── init.lua                                       -- main entry
├── utils.lua                                      -- mylib.utils
└── helpers.lua                                    -- mylib.helpers
-- mylib/init.lua
local M = {}
M.utils = require("mylib.utils")
M.helpers = require("mylib.helpers")

function M.main()
    -- ...
end

return M
local mylib = require("mylib")                     -- loads mylib/init.lua
mylib.utils.format(...)
mylib.main()

Module patterns

Standard table return

local M = {}

function M.foo() end
function M.bar() end

return M

Inline definitions

return {
    foo = function() end,
    bar = function() end
}

Function-style module (rare)

local M = {}

local function foo() end
local function bar() end

M.foo = foo
M.bar = bar

return M

Single-function module

local function process(input)
    -- ...
end

return process
local process = require("processor")
process(input)

LuaRocks

LuaRocks is the conventional contemporary package manager:

luarocks install luasocket
luarocks install penlight
luarocks list                                       -- installed
luarocks search keyword
luarocks remove package

A rockspec describes a package:

-- mylib-1.0-1.rockspec
package = "mylib"
version = "1.0-1"
source = {
    url = "git+https://github.com/user/mylib"
}
description = {
    summary = "A useful Lua library"
}
dependencies = {
    "lua >= 5.1",
    "luasocket >= 3.0"
}
build = {
    type = "builtin",
    modules = {
        ["mylib"] = "src/mylib.lua",
        ["mylib.utils"] = "src/mylib/utils.lua"
    }
}

The conventional discipline:

  • Develop in a project directory.
  • Test locally with luarocks make.
  • Publish via luarocks upload.

Common patterns

Module with private state

-- File: cache.lua
local M = {}

local data = {}

function M.set(key, value)
    data[key] = value
end

function M.get(key)
    return data[key]
end

function M.clear()
    data = {}
end

return M

Module with class

-- File: queue.lua
local Queue = {}
Queue.__index = Queue

function Queue.new()
    return setmetatable({items = {}, head = 1, tail = 1}, Queue)
end

function Queue:push(item)
    self.items[self.tail] = item
    self.tail = self.tail + 1
end

function Queue:pop()
    if self.head < self.tail then
        local item = self.items[self.head]
        self.items[self.head] = nil
        self.head = self.head + 1
        return item
    end
end

function Queue:empty()
    return self.head >= self.tail
end

return Queue
local Queue = require("queue")
local q = Queue.new()
q:push("a")
q:push("b")
print(q:pop())                                     -- "a"

Module returning a function

-- File: greet.lua
return function(name)
    return "Hello, " .. name
end
local greet = require("greet")
print(greet("Alice"))                              -- "Hello, Alice"

Module with submodules

-- File: mymath/init.lua
local M = {}

M.basic = require("mymath.basic")
M.advanced = require("mymath.advanced")

return M
-- File: mymath/basic.lua
return {
    add = function(a, b) return a + b end,
    sub = function(a, b) return a - b end
}
local mymath = require("mymath")
print(mymath.basic.add(2, 3))                     -- 5

Configuration loading

-- File: config.lua
local config_file = arg[1] or "config.lua"

local M = {}

local function load_config(path)
    local fn, err = loadfile(path)
    if not fn then return {}, err end

    local sandbox = {}
    debug.setupvalue(fn, 1, sandbox)
    fn()
    return sandbox
end

local config = load_config(config_file)

M.host = config.host or "localhost"
M.port = config.port or 8080
M.debug = config.debug or false

return M

Module as namespace

-- File: utils.lua
local M = {}

M.string = {
    trim = function(s) return s:gsub("^%s*(.-)%s*$", "%1") end,
    split = function(s, sep)
        local parts = {}
        for part in s:gmatch("[^" .. sep .. "]+") do
            parts[#parts + 1] = part
        end
        return parts
    end
}

M.table = {
    deep_copy = function(t)
        if type(t) ~= "table" then return t end
        local result = {}
        for k, v in pairs(t) do result[M.table.deep_copy(k)] = M.table.deep_copy(v) end
        return result
    end
}

return M
local utils = require("utils")
print(utils.string.trim("  hello  "))              -- "hello"

Lazy loading via require cache

-- File: lazy.lua
return setmetatable({}, {
    __index = function(_, key)
        return require("lazy_implementations." .. key)
    end
})
local lazy = require("lazy")
print(lazy.something)                              -- loads lazy_implementations.something

The mechanism admits substantial deferred loading.

Module override via package.loaded

-- For testing — replace a module:
package.loaded["http_client"] = mock_http_client

-- Then code that calls require("http_client") gets the mock
local client = require("http_client")

Forwarding require

-- File: shorthand.lua
return {
    api = require("very.long.package.name.api"),
    utils = require("very.long.package.name.utils")
}

require with relative paths (5.4+)

The require admits relative module paths in Lua 5.4 (limited support):

-- Within a package:
local utils = require("..utils")                   -- sibling module

The mechanism is implementation-specific; the conventional discipline uses absolute module names.

Reloading modules

-- Force reload (useful in REPL):
package.loaded["mymodule"] = nil
local mymodule = require("mymodule")

The package.loaded cache admits explicit invalidation; conventional in development workflows.

Plugin pattern

-- File: plugins/init.lua
local M = {}

M.plugins = {}

function M.register(plugin)
    M.plugins[plugin.name] = plugin
    if plugin.on_load then plugin.on_load() end
end

function M.run(event, ...)
    for _, plugin in pairs(M.plugins) do
        if plugin[event] then plugin[event](...) end
    end
end

return M
-- File: plugins/logger.lua
local plugins = require("plugins")

plugins.register({
    name = "logger",
    on_request = function(req) print("request:", req) end,
    on_response = function(resp) print("response:", resp) end
})

Module with __index for missing attributes

local M = setmetatable({}, {
    __index = function(_, key)
        error("attempt to access undefined module member: " .. key, 2)
    end
})

function M.foo() return "foo" end
function M.bar() return "bar" end

return M

Versioned API

-- File: mylib/init.lua
local M = {}

M._VERSION = "1.0.0"

function M.version_info()
    return {
        version = M._VERSION,
        lua = _VERSION
    }
end

return M

The _VERSION is the conventional Lua module version field.

Cross-platform module

-- File: platform.lua
local M = {}

local function detect()
    if package.config:sub(1, 1) == "\\" then
        return "windows"
    else
        return "unix"
    end
end

M.os = detect()
M.path_sep = (M.os == "windows") and "\\" or "/"
M.line_sep = (M.os == "windows") and "\r\n" or "\n"

return M

The package.config admits substantial platform detection.

A note on module()

Lua 5.1 had a module() function for declaring modules; removed in 5.2+:

-- Lua 5.1 only:
module("mymodule", package.seeall)

function foo() end                                 -- becomes mymodule.foo

-- Modern equivalent (5.2+):
local M = {}
function M.foo() end
return M

The conventional contemporary discipline always uses the local M = {}; ... return M pattern — admit substantial portability.

A note on the conventional discipline

The contemporary Lua modules-and-packaging advice:

  • Use the local M = {}; ... return M pattern.
  • Use local for module-private.
  • Use require for loading; admit substantial caching.
  • Use submodules via dot-paths (mylib.utils).
  • Use init.lua for package main files.
  • Use LuaRocks for distribution.
  • Cache hot modules as locals: local string_format = string.format.
  • Use _VERSION field for version info.
  • Avoid module() — removed in 5.2+.
  • Test with package.loaded overrides.

The combination — file-as-module returning a table, require for loading with caching, the package.path/package.cpath search mechanism, the LuaRocks ecosystem, the conventional local M = {}; ... return M pattern — is the substance of Lua’s package management. The discipline produces clear, well-encapsulated, distributable code with substantial flexibility through the small core mechanism.