Learn Lua in Neovim: Variable Scope — local Keyword, Block Scope & Always Use Local | Episode 14
Video: Learn Lua in Neovim: Variable Scope — local Keyword, Block Scope & Always Use Local | Episode 14 by Taught by Celeste AI - AI Coding Coach
Lua Variable Scope: local, Block Scope, and Always Use Local
x = 10creates a global.local y = 20creates a local — visible only inside the current block (function, loop, orif). The strongest tip in this whole series: always uselocal.
Lua's scoping rules are simple but unusual. Variables are global by default — the opposite of most modern languages. The local keyword is what saves you from global-variable chaos. This episode is about why and how.
Global vs local
x = 10 -- global: visible everywhere
local y = 20 -- local: only visible in this scope
print("x = " .. x)
print("y = " .. y)
Both work, both print. The difference is invisible at the top level. It only matters when scopes start nesting.
Block scope
if true then
local secret = "hidden"
print("Inside: " .. secret)
end
-- print(secret) -- would be nil out here!
Control structures (if, for, while, repeat, function bodies) introduce a new scope. Locals declared inside that scope vanish when the block ends.
Same with loops:
for i = 1, 3 do
print("i = " .. i)
end
-- print(i) -- nil out here too!
The loop variable i is local to the for. After the loop, i doesn't exist.
Shadowing
local color = "blue"
print("Outer: " .. color)
if true then
local color = "red" -- new local, shadows the outer
print("Inner: " .. color)
end
print("Outer again: " .. color)
Output:
Outer: blue
Inner: red
Outer again: blue
The local color = "red" inside the if creates a new local that hides the outer color for the duration of the if block. When the block ends, the outer color is visible again.
Shadowing is occasionally useful but often a bug source. If you didn't mean to shadow, rename the inner variable.
"Always use local"
function calculate_tax(amount)
rate = 0.08 -- BUG: creates a global!
return amount * rate
end
calculate_tax(100)
print(rate) -- 0.08 — leaked!
That rate = 0.08 looks innocent but creates a global visible everywhere. Months later someone reads print(rate) and gets the leftover value, or another function clobbers it.
The fix:
function calculate_tax(amount)
local rate = 0.08
return amount * rate
end
local rate keeps rate confined to this function. No leak.
The strongest single rule: always use local unless you specifically want a global. Lua's defaults betray you here; counter the default with discipline.
Linters help
Tools like luacheck flag implicit globals:
calculate_tax.lua:2:3: setting non-standard global variable 'rate'
If you write Lua professionally, set up a linter. Saves a lot of debugging.
Locals are faster, too
Lua reads local variables directly from the stack; globals go through a table lookup (the global environment). For hot loops, locals are measurably faster:
-- slow: math.sqrt is a global lookup every iteration
for i = 1, 1_000_000 do
local x = math.sqrt(i)
end
-- faster: cache to a local, lookup once
local sqrt = math.sqrt
for i = 1, 1_000_000 do
local x = sqrt(i)
end
For most code, the difference is invisible. For inner loops in performance-critical code, this caching idiom is standard.
Function parameters are locals
function greet(name)
-- `name` is local to this function
name = name .. "!"
print(name)
end
local n = "Alice"
greet(n)
print(n) -- still "Alice"
name is a parameter — implicitly local. Mutating it inside the function doesn't affect the caller's variable.
(Unless the value is a table — then both names point to the same table, and mutations are visible. Tables use reference semantics; we cover that in episode 22.)
Nested function scope
function outer()
local x = 10
function inner()
print(x) -- can read outer's x
end
inner()
end
Inner functions can read variables from enclosing scopes. This is what makes closures (next-next episode, episode 16) work.
When to use globals
In modern Lua code, almost never. The exceptions:
- Module returns at the top level of a file — though even those should be
local M = {}andreturn M(covered in episode 24). - Configuration values that genuinely need to be visible everywhere — better expressed as a global table:
config = { debug = true }. - Quick scripts where scope discipline doesn't pay off.
For anything bigger than a 10-line script, treat global variables as a code smell.
Common stumbles
Forgetting local. Most common scoping mistake. Read your code looking for name = ... without local and audit each one.
Shadowing accidentally. A function parameter and an outer variable with the same name confuse readers. Rename one.
Reading after the loop. for i = ... print(i) after the loop is nil. Copy the value out: local last; for i = ... do last = i end.
Globals in modules. A module file that does count = 0 adds count to the global namespace, conflicting with anyone else's count. Always local count = 0 inside modules.
local in a for declaration. for local i = ... is a syntax error. The loop variable is automatically local; no keyword needed.
What's next
Episode 15: first-class functions. Functions are values you can store, pass, and return. We build a filter function that selects items from a list using a callback.
Recap
Variables are global by default in Lua — the opposite of most languages. Always use local to confine variables to the current scope. Blocks (if, for, while, function bodies) introduce new scopes; locals declared inside them disappear when the block ends. Inner scopes can shadow outer ones. Function parameters are implicit locals. Locals are also faster than globals (stack vs table lookup). Use a linter to catch implicit globals.
Next episode: first-class functions.