Scope
Lua’s scoping rule is simple: variables are global by default; the local keyword introduces block-scoped locals. The conventional discipline uses local for almost everything — globals are conventionally avoided except for module exports. Functions admit lexical scoping with closures over upvalues (variables in enclosing function scopes). The _G table contains all globals; _ENV (5.2+) admits substantial environment manipulation. The combination — global-by-default with explicit local, lexical block scoping, the upvalue mechanism for closures, the _G/_ENV for globals introspection — is the substance of Lua’s scope model.
Global vs local
x = 10 -- global
local y = 20 -- local
function foo()
z = 30 -- still global!
local w = 40 -- local to foo
end
The local keyword admits block-scoped variables; assignment without local creates or modifies a global.
The conventional contemporary discipline:
- Use
localfor almost everything. - Globals are conventionally module-exposed names only.
Block scope
local variables are scoped to the enclosing block:
local x = 5
do
local y = 10 -- block-scoped
print(x, y) -- 5, 10
end
print(x) -- 5
print(y) -- nil (out of scope)
The principal blocks:
- function bodies —
function ... end. - control structures —
if/then/end,while/do/end,for/do/end,repeat/until. - explicit
do/endblocks.
for i = 1, 10 do
local x = i * 2
print(x)
end
print(x) -- nil (out of scope)
print(i) -- nil (loop variable also local)
local function
The form local function name(params) ... end is sugar for:
local function f(x)
return x * 2
end
-- Approximately equivalent to:
local f = function(x)
return x * 2
end
The local function form admits recursive references to the function (the binding is in scope inside the body):
local function factorial(n)
if n <= 1 then return 1 end
return n * factorial(n - 1) -- self-reference works
end
The non-function-keyword form requires forward declaration for recursion:
local factorial
factorial = function(n)
if n <= 1 then return 1 end
return n * factorial(n - 1)
end
The conventional contemporary discipline uses local function.
Upvalues and closures
Lua functions admit closures over their enclosing scope’s locals — these captured variables are called upvalues:
local function make_counter()
local count = 0
return function()
count = count + 1 -- count is an upvalue
return count
end
end
local counter = make_counter()
print(counter()) -- 1
print(counter()) -- 2
print(counter()) -- 3
Each call to make_counter produces a new closure with its own count. The upvalues admit substantial state encapsulation without classes.
For shared upvalues:
local function make_pair()
local x = 0
return function() x = x + 1; return x end, -- both functions share x
function() return x end
end
local incr, get = make_pair()
incr()
incr()
print(get()) -- 2
Variable shadowing
Inner local declarations shadow outer:
local x = 5
do
local x = 10 -- shadows outer
print(x) -- 10
end
print(x) -- 5
Shadowing is admitted; the conventional discipline avoids it for clarity except in narrow cases.
_G — the globals table
All globals live in the _G table:
x = 42 -- create global
print(_G.x) -- 42
-- Iterating all globals:
for k, v in pairs(_G) do
print(k, v) -- prints all global names
end
-- Setting via _G:
_G.y = 100
print(y) -- 100
The mechanism admits substantial introspection but is rarely needed in routine code.
_ENV (5.2+)
The _ENV is the current environment — admit substantial scope manipulation:
local function with_env(t, fn)
local _ENV = t -- replace environment
return fn()
end
-- All "global" references in fn() now look up in t.
The _ENV admits sandboxing and DSL-like environments — admit substantial customisation.
do ... end
The explicit block:
do
local temp = compute_expensive()
process(temp)
end
-- temp not visible here; freed for GC
The conventional uses are:
- Scoping locals tightly — admit substantial cleanup.
- Grouping for clarity.
Loop variable scope
for i = 1, 10 do
-- i is local to this iteration
end
print(i) -- nil
for k, v in pairs(t) do
-- k, v local to iteration
end
while x > 0 do
local y = compute(x) -- new y per iteration
x = y
end
Loop variables are implicitly local.
repeat ... until
The until condition has visibility into the block’s locals:
local n = 0
repeat
local doubled = n * 2
n = n + 1
until doubled > 100 -- doubled visible here!
The form is unique among Lua control structures — admits “condition computed in the loop body”.
Module scope
A module (file) admits its own global namespace through the conventional pattern:
-- File: mymodule.lua
local M = {}
local function helper() -- module-private
-- ...
end
function M.public_function(x)
return helper(x) * 2
end
M.constant = 42
return M
Importing:
-- File: main.lua
local mymodule = require("mymodule")
mymodule.public_function(5)
print(mymodule.constant)
The local declarations are module-private; the returned table is the public API. Treated in Modules and packaging.
Common patterns
local everywhere
-- Bad (creates globals):
function compute(x)
result = x * 2 -- global!
return result
end
-- Good:
local function compute(x)
local result = x * 2
return result
end
The conventional discipline applies local to:
- All local variables.
- All “private” functions.
- All function parameters and returns (implicitly local).
Caching globals as locals
For substantial performance in hot loops:
local print = print -- local alias
local string_format = string.format
local math_floor = math.floor
for i = 1, 1000000 do
print(string_format("%d", math_floor(i / 2)))
end
The local alias admits substantial speed improvement — Lua’s local-variable access is faster than global lookup.
Closure for state encapsulation
local function make_observable(initial)
local value = initial
local listeners = {}
local function subscribe(listener)
listeners[#listeners + 1] = listener
end
local function set(new_value)
if new_value ~= value then
value = new_value
for _, listener in ipairs(listeners) do
listener(new_value)
end
end
end
local function get()
return value
end
return {subscribe = subscribe, set = set, get = get}
end
local counter = make_observable(0)
counter.subscribe(function(v) print("changed to", v) end)
counter.set(5) -- "changed to 5"
counter.set(10) -- "changed to 10"
The closure admits substantial encapsulated state without classes.
Memoization
local function memoize(f)
local cache = {}
return function(x)
if cache[x] == nil then
cache[x] = f(x)
end
return cache[x]
end
end
local fib = memoize(function(n)
if n <= 1 then return n end
return fib(n - 1) + fib(n - 2)
end)
Module pattern
local M = {}
-- Private state:
local cache = {}
-- Private function:
local function compute(x)
return x * 2 + 1
end
-- Public API:
function M.process(x)
if cache[x] then return cache[x] end
local result = compute(x)
cache[x] = result
return result
end
function M.clear_cache()
cache = {}
end
return M
Const-like via local
local PI = 3.14159
local MAX_RETRIES = 3
-- Lua 5.4 admits the <const> attribute:
local PI <const> = 3.14159
The <const> (5.4+) admits compile-time enforcement; pre-5.4 the local is conventionally treated as constant.
Sandboxed execution
local sandbox_env = {
print = print,
pairs = pairs,
ipairs = ipairs,
-- ... selected globals ...
}
local code = "x = 5; print(x)"
local fn = load(code, "sandbox", "t", sandbox_env)
if fn then fn() end
The mechanism admits substantial isolation — load with a controlled environment.
do ... end for cleanup
do
local f = io.open("data.txt", "r")
if f then
local content = f:read("*a")
f:close()
process(content)
end
end
-- f and content out of scope; available for GC
For Lua 5.4+, the <close> attribute admits substantial automatic cleanup:
do
local f <close> = io.open("data.txt", "r")
-- f:close() called automatically when scope exits
if f then process(f:read("*a")) end
end
Mutually recursive functions
local odd, even
odd = function(n)
if n == 0 then return false end
return even(n - 1)
end
even = function(n)
if n == 0 then return true end
return odd(n - 1)
end
print(even(10)) -- true
print(odd(7)) -- true
The forward declaration admits substantial mutual recursion.
Iterator with closure
local function count_up(max)
local i = 0
return function()
i = i + 1
if i <= max then return i end
end
end
for n in count_up(5) do
print(n) -- 1, 2, 3, 4, 5
end
Treated in Iterators.
Configuration via environment
local function load_config(filename)
local config = {}
local fn, err = loadfile(filename, "t", config)
if not fn then return nil, err end
fn() -- runs in `config` environment
return config
end
-- File: settings.lua
host = "localhost"
port = 8080
debug = true
-- Loading:
local cfg = load_config("settings.lua")
print(cfg.host, cfg.port, cfg.debug)
The _ENV-based mechanism admits substantial DSL-style configuration.
A note on global avoidance
Globals are conventionally avoided for several reasons:
- Performance — global lookup is slower than local.
- Clarity — explicit
localadmits substantial readability. - Safety — accidental globals (typos in variable names) are common bugs.
- Module hygiene — globals from one module pollute the global namespace.
The conventional defence:
- Use
localeverywhere. - Use linters (luacheck) to detect accidental globals.
- Use module patterns — return tables.
- Use
strict.luain 5.1 for runtime detection of typo’d globals.
A note on the conventional discipline
The contemporary Lua scope advice:
- Use
localfor everything — globals are exceptional. - Use
local functionfor declarations. - Use closures for state encapsulation.
- Use the module pattern (
local M = {}; ... return M). - Cache hot globals as locals for performance.
- Use
<const>(5.4+) for compile-time constants. - Use
<close>(5.4+) for substantial RAII-like cleanup. - Use
do ... endfor tight scoping. - Use
_ENV(5.2+) for substantial sandboxing. - Use linters to detect accidental globals.
The combination — explicit local declarations, block-scoped lexical visibility, closures over upvalues, the global table _G and the environment _ENV, the <const> and <close> attributes (5.4+) — is the substance of Lua’s scope model. The discipline produces clear, well-encapsulated code with substantial control over visibility.