Part of Learn Lua with NeoVim

Learn Lua in Neovim: Variable Scope — local Keyword, Block Scope & Always Use Local | Episode 14

Sandy LaneSandy Lane

Video: Learn Lua in Neovim: Variable Scope — local Keyword, Block Scope & Always Use Local | Episode 14 by Taught by Celeste AI - AI Coding Coach

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

Lua Variable Scope: local, Block Scope, and Always Use Local

x = 10 creates a global. local y = 20 creates a local — visible only inside the current block (function, loop, or if). The strongest tip in this whole series: always use local.

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 = {} and return 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.

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.