Modules and require
Reviewed & published by Brayan K
By the end of this lesson you'll split a program across files: write a module that returns a table, load it with require (which runs once and caches), understand package.path and dotted module names, prefer local over globals to keep the namespace clean, and know why the old module() function is gone.
Part of the free Lua course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn
- Write a module as a file that returns a table
- Load modules with require and reuse the cached result
- Understand that require runs a module's code only once
- Use package.path and dotted names to organise files in folders
- Keep internals local to avoid polluting the global namespace
- Recognise and replace the deprecated module() function
💡 Real-World Analogy
A module is a toolbox you keep in one drawer. Inside, the table it returns is the tray of tools you actually hand out; the local helpers are the messy offcuts you keep in the back, out of sight. require is the act of fetching that drawer — and because the workshop is tidy, the first person to open it sets it on the bench (runs it once) and everyone after just borrows the same opened toolbox (the cache). Organise your tools into labelled drawers (folders and dotted names) and the whole workshop stays findable.
1. A Module Returns a Table
In modern Lua a module is simply a file that builds a table of everything it wants to share and returns it on the last line. Public functions and constants go on that table; private helpers stay as local functions so they never leak out. There is no special module keyword — the file is ordinary Lua, and the returned table is its public interface. Read this mymath.lua example and notice how the private clamp helper stays hidden.
-- FILE: mymath.lua
-- A module is just a normal Lua file that RETURNS a table of
-- everything it wants to share. Keep helpers 'local' so they
-- don't leak into the global namespace.
local mymath = {} -- the table we'll hand back
-- A private helper — local, so it is NOT visible to importers.
local function clamp(x)
if x < 0 then return 0 end
return x
end
-- Public functions go on the table.
function mymath.add(a, b)
return a + b
end
function mymath.square(n)
return clamp(n) * clamp(n) -- can use the private helper
end
mymath.PI = 3.14159 -- public constant
return mymath -- the last line returns the table2. Loading with require (and Caching)
require("name") finds the module's file, runs it once, and returns whatever it returned — no .lua extension in the name. The crucial detail is caching: require stores the result in package.loaded, so a second require of the same name returns the identical table without re-running the file. That is why a shared config module behaves as one object across your whole program: change it in one place and everyone who required it sees the change.
-- FILE: main.lua (same folder as mymath.lua)
-- require finds the file, RUNS it once, and returns its table.
local mymath = require("mymath") -- note: no ".lua" extension
print(mymath.add(2, 3)) -- prints: 5
print(mymath.square(4)) -- prints: 16
print(mymath.PI) -- prints: 3.14159
-- CACHING: require runs a module's code only ONCE.
-- A second require returns the SAME cached table, it does not re-run.
local again = require("mymath")
print(again == mymath) -- prints: true (identical table)
-- This is why a "config" module shared across files gives everyone
-- the same single table — handy for shared settings.
local config = require("mymath")
config.PI = 3.14 -- everyone who required it sees this change
print(mymath.PI) -- prints: 3.14Your turn. Finish the greet module: uppercase a greeting and, crucially, return the table. Fill in the blanks marked ___.
-- 🎯 YOUR TURN — finish the 'greet' module, then return it.
-- FILE: greet.lua
local greet = {}
-- 1) Add a public function hello(name) that returns "Hi, <name>!"
function greet.hello(name)
return "Hi, " .. name .. "!"
end
-- 2) Add a public function loud(name) returning the hello in UPPERCASE
function greet.loud(name)
return greet.hello(name):___() -- 👉 the string method that uppercases
end
-- 3) The LAST line must hand the table back to importers
___ greet -- 👉 the keyword that returns it
-- ✅ When required and called:
-- local g = require("greet")
-- print(g.hello("Ada")) -- Hi, Ada!
-- print(g.loud("Ada")) -- HI, ADA!3. package.path, Dotted Names & Avoiding Globals
require locates files through package.path, a list of templates where ? is replaced by the module name. Dotted names map dots to folders, so require("shapes.circle") looks for shapes/circle.lua — perfect for organising larger projects. Always assign a required module to a local variable: it keeps dependencies scoped and avoids polluting _G, the global table. The same discipline applies inside modules — keep helpers local so they never collide with names elsewhere.
-- HOW require FINDS FILES: package.path is a ';'-separated list of
-- templates where '?' is replaced by the module name. For example:
-- ./?.lua;/usr/local/share/lua/5.4/?.lua
-- require("mymath") tries ./mymath.lua, then the next template, etc.
print(package.path) -- shows your current search templates
-- DOTTED NAMES map dots to directory separators by default:
-- require("shapes.circle") looks for shapes/circle.lua
-- This lets you organise modules into folders.
-- local circle = require("shapes.circle")
-- You can extend the search path at runtime by prepending a template:
package.path = "./lib/?.lua;" .. package.path
-- now require also looks inside a ./lib folder
-- LOCAL vs GLOBAL: assign the result of require to a LOCAL variable.
-- This keeps your dependency names scoped and fast, and avoids
-- polluting _G (the global table) with every module you load.
local json = require("mymath") -- good: scoped to this file
-- json = require("mymath") -- avoid: leaks a global named 'json'Now you try loading. Require the greet module into a local and prove that a second require returns the same cached table. Fill in the blanks:
-- 🎯 YOUR TURN — load a module and prove it is cached.
-- FILE: main.lua
-- 1) Load the 'greet' module into a LOCAL variable (no .lua extension)
local g = ___("greet") -- 👉 the function that loads a module
print(g.hello("Lin")) -- ✅ Expected: Hi, Lin!
-- 2) Require it a second time; caching means you get the SAME table
local g2 = require("greet")
print(g2 == ___) -- 👉 compare to the first table
-- ✅ Expected output:
-- Hi, Lin!
-- truePro Tips
- 💡 Return one table. Build a local table, attach your public functions, and return it on the last line — that is the whole module pattern.
- 💡 Trust the cache. Because require runs a module once, use a shared module for config, loggers, and singletons instead of passing them around.
- 💡 Always use local for required modules. local json = require("json") scopes the dependency and avoids a stray global.
- 💡 Organise with folders. Dotted names like require("net.http") map to net/http.lua, keeping big projects navigable.
Common Errors (and the fix)
- "module 'x' not found": the file is not on package.path. Fix: run from the right folder, use the correct dotted name, or prepend a template to package.path.
- Forgot to return the table: the module file ends without return mymod, so require hands back true and your functions are missing. Fix: make the last line return mymod.
- Included the extension: writing require("mymath.lua") makes Lua look for mymath/lua.lua. Fix: drop the .lua — use require("mymath").
- Leaking globals: writing function helper() without local inside a module pollutes _G. Fix: declare private helpers as local function.
📋 Quick Reference
| Task | Code | Result / Note |
|---|---|---|
| Define module | table is the interface | |
| Load module | local M = require("name") | no .lua, cached |
| Private helper | local function help() end | stays hidden |
| Folder/module | require("net.http") | net/http.lua |
| Search path | package.path | ? templates |
| Cache table | package.loaded | already-loaded modules |
Mini-Challenge: a stack module
No blanks this time — just a brief and an outline. You'll package a small class into its own file and consume it from another via require, keeping internals local and returning only the table. Build both files and run main.lua.
-- 🎯 MINI-CHALLENGE: a small 'stack' module
-- FILE: stack.lua
-- 1. local Stack = {} and Stack.__index = Stack
-- 2. function Stack.new() returns setmetatable({ items = {} }, Stack)
-- 3. Stack:push(v) -> table.insert(self.items, v)
-- 4. Stack:pop() -> return table.remove(self.items)
-- 5. The last line: return Stack
--
-- FILE: main.lua
-- local Stack = require("stack")
-- local s = Stack.new()
-- s:push(1); s:push(2)
-- print(s:pop()) -- ✅ 2
--
-- Keep every helper local; only the returned table should be public.
-- your code here🎉 Lesson Complete!
- ✅ A module is a file that builds a table and returns it on the last line
- ✅ require("name") runs the file once and caches the result in package.loaded
- ✅ A second require returns the same shared table — great for config and singletons
- ✅ package.path templates and dotted names locate files, even inside folders
- ✅ Use local for required modules and internal helpers to avoid global pollution
Practice quiz
What is the standard way for a Lua module to expose its contents?
- By returning a table at the end of the file
- By setting many globals
- By calling export()
- By printing them
Answer: By returning a table at the end of the file. A module file builds a table of its public functions and returns it as the last statement.
Which function loads a module by name?
- import
- include
- require
- load
Answer: require. require('name') finds, runs, and returns the module, caching the result.
How many times does require run a given module's code, no matter how often you require it?
- Never, it only links
- Once, then it is cached
- Every time you call require
- Twice for safety
Answer: Once, then it is cached. require runs the module body once and caches the return value in package.loaded for reuse.
Where does require remember already-loaded modules?
- _G
- package.path
- package.cpath
- package.loaded
Answer: package.loaded. package.loaded is the cache table keyed by module name; a present entry skips re-running.
What does package.path control?
- The search patterns for finding Lua module files
- The OS PATH variable
- The location of the lua binary
- Garbage collection timing
Answer: The search patterns for finding Lua module files. package.path is a list of ? templates that require expands to locate a module's source file.
How is the dotted name require('a.b.c') usually mapped to a file?
- It is ignored
- Dots become directory separators: a/b/c.lua
- It loads a.lua only
- It looks for a-b-c.lua
Answer: Dots become directory separators: a/b/c.lua. By default each dot maps to a path separator, so a.b.c is sought as a/b/c.lua via package.path.
Why should module code declare its internals as local?
- require requires it
- locals run slower but are clearer
- Because globals are illegal in modules
- To avoid leaking globals that could clash with other code
Answer: To avoid leaking globals that could clash with other code. Keeping helpers local prevents polluting the global namespace and accidental name collisions.
What does require return to the caller?
- The module's file path
- Nothing
- The value the module returned (usually a table)
- Always true
Answer: The value the module returned (usually a table). require yields whatever the module returned, which is conventionally a table of functions.
What is the status of the old module() function in modern Lua?
- Required in every module
- Deprecated; return a table instead
- Only way to make modules
- Renamed to require
Answer: Deprecated; return a table instead. The module() function was deprecated after Lua 5.1; current style simply returns a table.
If two files both require('config'), how many config tables exist?
- One shared table from the cache
- Two separate copies
- Zero until both finish
- It depends on package.cpath
Answer: One shared table from the cache. Because require caches, both files receive the very same single table instance.
Continue this course
- Previous: Coroutines
- Next: Error Handling: pcall & error — Lua's protected-call model — pcall, xpcall, error and assert
Frequently asked questions
How do I create a module in modern Lua?
Make an ordinary .lua file, build a local table holding the functions and values you want to expose, and return that table as the file's last statement. Keep any private helpers as local functions so they stay hidden. There is no special syntax or keyword: a module is simply a file that returns a table. Callers then write local mymod = require('mymod') and use mymod.fn(). This pattern replaced the old module() function and is the style you will see in virtually all current Lua code.
Does require run the module file every time I call it?
No. require runs a module's code exactly once and caches the returned value in the package.loaded table, keyed by the module name. Every later require of the same name skips running the file and hands back the cached value immediately. This makes loading cheap and, importantly, means a shared module such as a config or a logger is a single shared object: if two files require it and one modifies it, the other sees the change because they hold the very same table.
How does require turn a module name into a file on disk?
It searches using package.path, a string of semicolon-separated templates where a question mark stands in for the module name. For require('mymath') and a template ./?.lua, it looks for ./mymath.lua; if that misses it tries the next template. Dotted names map dots to directory separators, so require('shapes.circle') is sought as shapes/circle.lua. You can add locations by prepending your own template to package.path, for example './lib/?.lua;' .. package.path.
Why should I store dependencies in local variables and avoid globals?
Assigning require's result to a local keeps each file's dependencies scoped to that file, which is clearer and slightly faster to access than a global. More importantly, declaring a module's internal helpers as local stops them leaking into _G, the global table, where they could clash with names from other modules or be accidentally overwritten. Disciplined use of local is the main defence against the subtle bugs that come from a polluted global namespace in a large codebase.
What was the module() function and why is it gone?
In Lua 5.1 there was a module() function that you called at the top of a file to declare it as a module; it set up a table and could change the file's environment. It was error-prone and had surprising effects on globals, so it was deprecated and removed in later versions. Modern Lua dropped it entirely in favour of the simpler, explicit pattern: build a local table, fill it, and return it. If you see module(...) in old code, treat it as legacy and refactor to a returned table.