Tables, metatables and object orientation
How __index and __newindex turn plain tables into classes, proxies and read-only views.
References and copying
Tables are compared and assigned by reference. Two names can point at one table, and passing a table to a function passes the reference, so mutations are visible to the caller.
local a = { 1, 2 }
local b = a -- the same table, not a copy
b[3] = 3
print(#a) -- 3
local shallow = {}
for k, v in pairs(a) do shallow[k] = v end -- copy the top level only
shallow[1] = 99
print(a[1]) -- 1 (numbers are copied by value)
-- table.clone is standard in 5.4; a hand-written deep copy needs recursion
local function deepcopy(t, seen)
if type(t) ~= "table" then return t end
seen = seen or {}
if seen[t] then return seen[t] end
local out = {}
seen[t] = out
for k, v in pairs(t) do out[deepcopy(k, seen)] = deepcopy(v, seen) end
return out
end- Cyclic tables need the
seenmap above, or a self-referential table loops forever. - The length operator
#tis only reliable for a sequence with no holes. - Deleting a key is
t[k] = nil; there is no separate delete statement.
Metatables: __index and __newindex
A metatable is an ordinary table attached to another table. When a lookup misses, Lua consults __index; when an assignment targets a key that is not already present, Lua consults __newindex. Everything object-oriented in Lua is built from these two hooks.
local Stack = {}
Stack.__index = Stack -- instance lookups fall back to the class
function Stack.new()
return setmetatable({ items = {} }, Stack)
end
function Stack:push(v) -- ':' passes self implicitly
self.items[#self.items + 1] = v
end
function Stack:pop()
local v = self.items[#self.items]
self.items[#self.items] = nil
return v
end
function Stack:size() return #self.items end
local s = Stack.new()
s:push("a")
s:push("b")
print(s:pop(), s:size()) -- b 1
-- __index may also be a function: computed values and defaults
local defaults = setmetatable({}, {
__index = function(_, key)
return key == "timeout" and 30 or ("unknown: " .. key)
end,
})
print(defaults.timeout, defaults.host) -- 30 unknown: host
-- __newindex intercepts writes, which is how proxies are written
local log = {}
local proxy = setmetatable({}, {
__index = log,
__newindex = function(_, k, v) print("set", k); rawset(log, k, v) end,
})
proxy.x = 1 -- prints "set x", stores into log
print(log.x) -- 1
print(rawget(proxy, "x")) -- nil: the metamethod was bypassed__index defeats the fast path. Prefer a table (the class) as __index and reserve functions for computed values and proxies.Operator metamethods
| Metamethod | Fires when | Extra rule |
|---|---|---|
__index | Reading a missing key | Table or function |
__newindex | Assigning a missing key | Table or function; rawset bypasses it |
__call | The table is called like a function | Receives the table as the first argument |
__tostring | tostring/print | Must return a string, otherwise the tostring behaviour is skipped |
__len | The # operator | Lua 5.2+ only, and not used by table.insert |
__eq | == between two tables | Only when both operands share the same metamethod |
__lt, __le | < and <= | Used by table.sort with a default comparator |
__add, __sub, __mul, __concat | Arithmetic and .. | Either operand's metatable may supply it |
__gc, __close | Collection or scope exit | Requires setmetatable at creation time |
local Vec = {}
Vec.__index = Vec
Vec.__add = function(a, b) return Vec.new(a.x + b.x, a.y + b.y) end
Vec.__eq = function(a, b) return a.x == b.x and a.y == b.y end
Vec.__tostring = function(v) return "(" .. v.x .. ", " .. v.y .. ")" end
Vec.__len = function(v) return math.sqrt(v.x ^ 2 + v.y ^ 2) end
function Vec.new(x, y) return setmetatable({ x = x, y = y }, Vec) end
local p = Vec.new(1, 2) + Vec.new(3, 4)
print(p, #p) -- (4, 6) 7.211102550928
print(Vec.new(1, 1) == Vec.new(1, 1)) -- true
-- tostring only works because __tostring returned a string; assert that
local ok, text = pcall(tostring, setmetatable({}, { __tostring = function() return 0 end }))
print(ok, text ~= nil) -- false true (raises instead of looping)__index chains give inheritance: point one class's metatable at another class. Keep the chain shallow — every level that misses adds another table lookup, and deep chains make errors hard to trace.
FAQ
How do I make a read-only table?
setmetatable({}, { __index = data, __newindex = function() error("read-only", 2) end }). Writes fail while reads pass through. It is a guard against mistakes, not against hostile code, since rawset on the original table still works.Is there a class or new keyword?
__index back-reference, and setmetatable is the constructor's last step. Libraries such as middleclass add inheritance sugar, but the mechanism is always metatables.Related
Lua syntax and types Embedding Lua in a host program
Last refreshed 2026-09-18.