Lua with NeoVim: Modules and Require — local M = {}, return M & require() | Episode 24
Video: Lua with NeoVim: Modules and Require — local M = {}, return M & require() | Episode 24 by Taught by Celeste AI - AI Coding Coach
Lua Modules and Require: Splitting Code Across Files
A module is a Lua file that returns a table.
local M = {}at the top, attach functions,return Mat the end. Use it from another file withlocal mt = require("name").
So far every program has been a single file. As code grows, you split it into modules — files that group related functions and are imported by name. Today we cover the standard pattern.
A module file
-- mathtools.lua
local M = {}
function M.add(a, b)
return a + b
end
function M.subtract(a, b)
return a - b
end
function M.multiply(a, b)
return a * b
end
function M.square(x)
return x * x
end
function M.is_even(n)
return n % 2 == 0
end
return M
Three pieces:
local M = {}— create an empty table; this will be our module's exports.function M.foo(...) ... end— attach functions to the module table. Equivalent toM.foo = function(...) ... end.return M— at the end of the file, return the table. This is whatrequirewill return to the caller.
Anyone using this module sees only what's attached to M. Anything declared local inside the file (not on M) stays private.
Using the module
-- main.lua
local mt = require("mathtools")
print("add(10, 20): " .. mt.add(10, 20))
print("subtract(50, 15): " .. mt.subtract(50, 15))
print("multiply(6, 7): " .. mt.multiply(6, 7))
print("square(9): " .. mt.square(9))
print("is_even(4): " .. tostring(mt.is_even(4)))
print("is_even(7): " .. tostring(mt.is_even(7)))
require("mathtools") runs mathtools.lua (once) and returns whatever the file returned. We assign it to mt and call mt.add(...), mt.square(...), etc.
The string "mathtools" is the module name — Lua searches for mathtools.lua in the package path (package.path).
Run the program
From the directory containing both files:
lua main.lua
Output:
add(10, 20): 30
subtract(50, 15): 35
multiply(6, 7): 42
square(9): 81
is_even(4): true
is_even(7): false
require runs the file once
local a = require("mathtools")
local b = require("mathtools")
print(a == b) -- true (same table)
Lua caches loaded modules in package.loaded. The second require hits the cache and returns the same table — the file isn't re-executed.
This means modules can have initialisation code that runs once:
-- counter.lua
local M = {}
print("counter module loaded")
local count = 0
function M.next()
count = count + 1
return count
end
return M
The print("counter module loaded") runs the first time the file is required. The count is a private upvalue that all calls share.
Private functions
-- mathtools.lua
local M = {}
local function double(x) -- local: not exported
return x * 2
end
function M.quadruple(x)
return double(double(x)) -- can use the private function
end
return M
Private helpers stay local to the module. The exported API is just M.quadruple. double is invisible from outside.
This is module-level encapsulation — the unit of abstraction in Lua.
The package path
require("mathtools") searches package.path for the file. The default path is something like:
./?.lua;/usr/local/share/lua/5.4/?.lua;/usr/local/share/lua/5.4/?/init.lua;...
Each ? is replaced by the module name. So Lua looks for mathtools.lua in ., then /usr/local/share/lua/5.4/mathtools.lua, etc.
To add to the path:
package.path = package.path .. ";/my/custom/path/?.lua"
Or set the LUA_PATH environment variable.
Submodules
Module foo.bar lives in foo/bar.lua (or foo/bar/init.lua):
-- foo/bar.lua
local M = {}
function M.hello() print("from foo.bar") end
return M
local fb = require("foo.bar")
fb.hello()
Dots in require map to slashes in the filesystem. This is how big libraries organise: require("openssl.cipher"), etc.
Different export styles
The standard pattern is local M = {}; ...; return M. Variations:
Return a single function:
return function(x) return x * 2 end
local double = require("double")
print(double(5)) -- 10
Return without a M named table:
return {
add = function(a, b) return a + b end,
sub = function(a, b) return a - b end,
}
Same semantics. The named-table-then-attach style is more common because it lets you reference functions by name within the module.
package.loaded for testing
package.loaded["mathtools"] = nil -- clear the cache
local mt = require("mathtools") -- re-runs the file
Useful when developing a module — re-loading after edits without restarting the interpreter. In production, leave the cache alone.
init.lua and "module is a folder"
A module can be a folder if it contains init.lua:
mylib/
init.lua
helper.lua
require("mylib") -- loads mylib/init.lua
require("mylib.helper") -- loads mylib/helper.lua
Common for libraries with many submodules — init.lua re-exports them.
Avoiding global pollution
The cardinal sin of bad Lua modules:
-- bad-module.lua
add = function(a, b) return a + b end -- BUG: creates global!
return ...
Without local, add is now a global. Importing the module also sets your global add. Subtle, awful bugs.
The "always use local" rule from episode 14 applies double in modules. Lint your modules.
The legacy module() function
Old Lua 5.1 code uses:
module("mathtools", package.seeall)
function add(a, b) return a + b end
This was a misguided experiment that polluted globals and broke _ENV. Don't write new code with module(). The local M = {}; ...; return M pattern is the modern standard.
Common stumbles
Forgetting return M. The module returns nil from require. Crash on first use.
Not making helpers local. They leak as globals.
Importing the same module repeatedly thinking it re-runs. It doesn't — it's cached. Use package.loaded[name] = nil if you really want a re-load.
Module path mismatches. require("MathTools") won't find mathtools.lua (case-sensitive on Linux/macOS).
Circular requires. Module A requires B, B requires A. The second require returns whatever's been built so far — often nil. Refactor to break the cycle.
What's next
Episode 25: string patterns. Lua's regex-lite pattern language. %a, %d, %s, %w, quantifiers, string.match, string.gmatch. Extract values from text.
Recap
A module is a file that returns a table: local M = {}; ...; return M. Use with local mod = require("name"). require searches package.path and caches — file runs only once. Private helpers go in local function ...; exports go on M. Submodules: foo.bar → foo/bar.lua. Avoid the legacy module() function. Always local everything except your exports.
Next episode: string patterns.