Part of Learn Lua with NeoVim

Lua with NeoVim: Modules and Require — local M = {}, return M & require() | Episode 24

Sandy LaneSandy Lane

Video: Lua with NeoVim: Modules and Require — local M = {}, return M & require() | Episode 24 by Taught by Celeste AI - AI Coding Coach

Take the quiz on the full lesson page
Test what you've read · interactive walkthrough

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 M at the end. Use it from another file with local 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:

  1. local M = {} — create an empty table; this will be our module's exports.
  2. function M.foo(...) ... end — attach functions to the module table. Equivalent to M.foo = function(...) ... end.
  3. return M — at the end of the file, return the table. This is what require will 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.barfoo/bar.lua. Avoid the legacy module() function. Always local everything except your exports.

Next episode: string patterns.

Ready? Take the quiz on the full lesson page →
Test what you've learned. Watch the lesson and try the interactive quiz on the same page.