Metatables & Metamethods
Metatables let you customize the behavior of tables (and userdata). Every table can have one metatable. Metamethods are fields with __ prefix that define behavior for operations.
Setting metatables
local t = {}
local mt = {}
setmetatable(t, mt)
-- Get metatable
local mt = getmetatable(t)
-- Protected get (won't trigger __metatable)
local mt = rawgetmetatable(t)
[!tip]
__metatablefield If a metatable has__metatable,getmetatable()returns that value instead. Protects metatables from casual access.
Metamethod list
Arithmetic
| Metamethod | Operation | Notes |
|---|---|---|
__add | a + b | |
__sub | a - b | |
__mul | a * b | |
__div | a / b | |
__mod | a % b | |
__pow | a ^ b | |
__unm | -a | unary minus |
__idiv | a // b | floor division |
__band | a & b | bitwise AND |
__bor | a | b | bitwise OR |
__bxor | a ~ b | bitwise XOR |
__bnot | ~a | bitwise NOT |
__shl | a << b | left shift |
__shr | a >> b | right shift |
__concat | a .. b | concatenation |
Relational
| Metamethod | Operation | Notes |
|---|---|---|
__eq | a == b | only if same metamethod |
__lt | a < b | must define < not > |
__le | a <= b | must define <= not >= |
[!note]
__eqonly fires when both operands share the same metamethod function. Otherwise →false.
Table access
| Metamethod | Operation |
|---|---|
__index | Read missing key |
__newindex | Write to missing key |
__len | #t length |
__call | t(args) function call |
__gc | Garbage collection finalizer |
__close | Variable goes out of scope (<close> attribute) |
__mode | Weak references ("k", "v", "kv") |
__tostring | tostring(t) |
__index — reading missing keys
When you read a key that's nil in a table, LuaLuaA lightweight, embeddable scripting language used as the primary scripting language in FiveM server development. checks __index.
local proto = {hp = 100, mp = 50}
local mt = { __index = proto }
local player = setmetatable({}, mt)
print(player.hp) -- 100 (from proto)
print(player.mp) -- 50 (from proto)
__index as function
local mt = {
__index = function(t, key)
if key == "upper" then
return string.upper
end
end
}
local t = setmetatable({}, mt)
print(t.upper("hello")) -- HELLO
Prototype chain
local base = {x = 1}
local mid = setmetatable({y = 2}, {__index = base})
local top = setmetatable({z = 3}, {__index = mid})
print(top.x) -- 1 (follows chain: top → mid → base)
print(top.y) -- 2
print(top.z) -- 3
__newindex — writing missing keys
Fires when you assign to a key that doesn't exist in the table.
local mt = {
__newindex = function(t, key, value)
print("Setting " .. tostring(key) .. " = " .. tostring(value))
rawset(t, key, value) -- actually set it
end
}
local t = setmetatable({}, mt)
t.x = 10 -- prints: Setting x = 10
[!warning]
__newindexonly fires for new keys If the key already exists, the assignment is direct. Userawsetto bypass metamethods.
__call — callable tables
local mt = {
__call = function(t, ...)
print("Called with:", ...)
end
}
local f = setmetatable({}, mt)
f("hello", 42) -- Called with: hello 42
__tostring — string representation
local mt = {
__tostring = function(t)
return string.format("Point(%d, %d)", t.x, t.y)
end
}
local p = setmetatable({x = 3, y = 4}, mt)
print(p) -- Point(3, 4)
__gc — finalizers
local mt = {
__gc = function(t)
print("Goodbye!", t.name)
end
}
local o = setmetatable({name = "obj"}, mt)
o = nil
collectgarbage() -- prints: Goodbye! obj
See 10-Garbage-Collection for details.
__close — to-be-closed variables (5.4)
local mt = {
__close = function(t, err)
print("Closing:", t.name, err or "ok")
end
}
do
local handle <close> = setmetatable({name = "file"}, mt)
-- work with handle
end -- __close called here
__len — length operator
local mt = {
__len = function(t)
return t._count or 0
end
}
local t = setmetatable({_count = 42}, mt)
print(#t) -- 42
__eq, __lt, __le — comparisons
local mt = {
__eq = function(a, b) return a.value == b.value end,
__lt = function(a, b) return a.value < b.value end,
__le = function(a, b) return a.value <= b.value end,
}
local a = setmetatable({value = 1}, mt)
local b = setmetatable({value = 2}, mt)
print(a < b) -- true
print(a == b) -- false
Raw access (bypass metamethods)
rawget(t, key) -- read without __index
rawset(t, key, value) -- write without __newindex
rawequal(a, b) -- compare without __eq
rawlen(t) -- length without __len
Proxy table pattern
Use __index and __newindex together to intercept all access:
local data = {} -- real storage
local proxy = {}
local mt = {
__index = function(_, key)
print("GET", key)
return data[key]
end,
__newindex = function(_, key, value)
print("SET", key, value)
data[key] = value
end,
__pairs = function(_)
return pairs(data)
end
}
setmetatable(proxy, mt)
proxy.x = 10 -- SET x 10
print(proxy.x) -- GET x → 10
OOP with metatables
local Player = {}
Player.__index = Player
function Player.new(name, hp)
local self = setmetatable({}, Player)
self.name = name
self.hp = hp or 100
return self
end
function Player:damage(n)
self.hp = self.hp - n
if self.hp <= 0 then
print(self.name .. " died!")
end
end
function Player:__tostring()
return self.name .. " (HP: " .. self.hp .. ")"
end
-- Inheritance
local Warrior = setmetatable({}, {__index = Player})
Warrior.__index = Warrior
function Warrior.new(name)
local self = Player.new(name, 150) -- warriors have more HP
return setmetatable(self, Warrior)
end
local w = Warrior.new("Thor")
w:damage(10)
print(w) -- Thor (HP: 140)