Skip to content

Lua Metatables

Metatables are a powerful Lua feature that allows you to change the behavior of tables (and userdata) when certain operations are performed on them. Each table can have an associated metatable. A metatable itself is just a regular Lua table containing specific keys (called metamethods) that define how the original table should behave.

Key concepts involving metatables include:

  • Modifying table behavior for arithmetic, order, and concatenation operators.
  • Implementing object-oriented features like inheritance (__index).
  • Controlling access to table fields (__index, __newindex).
  • Customizing how tables are converted to strings (__tostring) or called like functions (__call).
  • setmetatable(table, metatable): Sets metatable as the metatable for table. Returns table. If metatable is nil, it removes the metatable of table.
  • getmetatable(table): Returns the metatable of table, or nil if it doesn’t have one or if the metatable has a __metatable field (which hides the metatable).
local my_table = {}
local my_metatable = { info = "This is a metatable" }
setmetatable(my_table, my_metatable)
local retrieved_mt = getmetatable(my_table)
print(retrieved_mt.info) -- Output: This is a metatable

If a metatable has a field named __metatable, its value will be returned by getmetatable instead of the actual metatable. This can be used to protect metatables from external access or modification.

local protected_table = {}
local actual_metatable = { __metatable = "Access Denied! This is not the real metatable." }
setmetatable(protected_table, actual_metatable)
print(getmetatable(protected_table)) -- Output: Access Denied! This is not the real metatable.

The __index metamethod is triggered when you try to access a key in a table that doesn’t exist (i.e., the value is nil).

It can be either a function or another table:

  • If __index is a function: Lua calls this function with the original table and the missing key as arguments: func(table, key). The function’s return value is the result of the access.
  • If __index is a table: Lua will look up the key in this __index table instead. This is commonly used for inheritance.

Example using __index as a function (default values):

local defaults = { x = 0, y = 0, name = "Default" }
local mt_index_func = {
__index = function(table, key)
print("Accessing missing key '" .. key .. "', returning default.")
return defaults[key] -- Return from a defaults table
end
}
local my_obj = { x = 10, z = 100 } -- y and name are missing
setmetatable(my_obj, mt_index_func)
print(my_obj.x) -- Output: 10 (exists in my_obj)
print(my_obj.y) -- Triggers __index: Output: Accessing missing key 'y', returning default. followed by 0
print(my_obj.name) -- Triggers __index: Output: Accessing missing key 'name', returning default. followed by Default
print(my_obj.z) -- Output: 100 (exists in my_obj)
print(my_obj.missing) -- Triggers __index: Output: Accessing missing key 'missing', returning default. followed by nil (as 'missing' not in defaults)

Example using __index as a table (simple inheritance):

local Parent = { greeting = "Hello from Parent" }
Parent.speak = function(self)
print(self.greeting)
end
local mt_index_table = { __index = Parent } -- If key not in Child, look in Parent
local Child = { specific_greeting = "Hi!" }
setmetatable(Child, mt_index_table)
print(Child.greeting) -- Output: Hello from Parent (found via __index to Parent)
Child:speak() -- Output: Hello from Parent (method found via __index)
print(Child.specific_greeting) -- Output: Hi! (found directly in Child)

The __newindex metamethod is triggered when you try to assign a value to a key in a table that doesn’t already exist (i.e., the key is nil in the table before assignment).

It can be either a function or another table:

  • If __newindex is a function: Lua calls this function with the original table, the key, and the value being assigned: func(table, key, value). The function is responsible for storing the value, perhaps with validation or redirection.
  • If __newindex is a table: Lua will perform the assignment in this __newindex table instead of the original table.

Example using __newindex as a function (read-only table or logging):

local data_store = {}
local mt_newindex_func = {
__newindex = function(table, key, value)
print("Attempting to set '" .. key .. "' to '" .. tostring(value) .. "'")
-- To make the table read-only for new keys:
-- error("Cannot add new keys to this table.", 2)
-- Or to log and store in a separate table:
print("Storing '" .. key .. "' in separate data_store.")
data_store[key] = value
end,
__index = function(table, key) -- To retrieve from data_store if logged
return data_store[key]
end
}
local my_config = { host = "localhost" }
setmetatable(my_config, mt_newindex_func)
my_config.host = "127.0.0.1" -- Updates existing key, __newindex not called
print("Host:", my_config.host) -- Output: Host: 127.0.0.1
my_config.port = 8080 -- New key, triggers __newindex
-- Output: Attempting to set 'port' to '8080'
-- Storing 'port' in separate data_store.
print("Port from my_config:", my_config.port) -- Output: Port from my_config: 8080 (via __index)
print("Port from data_store:", data_store.port) -- Output: Port from data_store: 8080

rawset(table, key, value) and rawget(table, key)

Section titled “rawset(table, key, value) and rawget(table, key)”

These functions allow you to set or get values in a table directly, bypassing the __index and __newindex metamethods. This is useful within metamethod implementations to avoid infinite recursion.

-- Example within a __newindex metamethod:
local mt_raw = {
__newindex = function(tbl, k, v)
print("__newindex called for key: ", k)
rawset(tbl, k, v) -- Use rawset to actually store the value in the original table
end
}
local t_raw = {}
setmetatable(t_raw, mt_raw)
t_raw.new_val = 100 -- Triggers __newindex
print(t_raw.new_val) -- Output: 100 (after __newindex prints its message)

You can define metamethods to overload arithmetic, bitwise (Lua 5.3+), concatenation, and comparison operators for tables.

MetamethodOperator/Operation
__add(a, b)Addition (+)
__sub(a, b)Subtraction (-)
__mul(a, b)Multiplication (*)
__div(a, b)Float Division (/)
__idiv(a, b)Floor Division (//) (Lua 5.3+)
__mod(a, b)Modulo (%)
__pow(a, b)Exponentiation (^)
__unm(a)Unary minus (negation, -a)
__concat(a, b)Concatenation (..)
__len(a)Length operator (#a)
__eq(a, b)Equality (==). Note: if __eq is not provided, a == b is true only if a and b are the same object. For ~=, Lua uses the result of __eq.
__lt(a, b)Less than (<)
__le(a, b)Less than or equal (<=). If not provided, Lua tries not (b < a).
__band(a,b)Bitwise AND (&) (Lua 5.3+)
__bor(a,b)Bitwise OR (|) (Lua 5.3+)
__bxor(a,b)Bitwise XOR (~) (Lua 5.3+)
__bnot(a)Bitwise NOT (~a) (Lua 5.3+)
__shl(a,b)Bitwise Left Shift (<<) (Lua 5.3+)
__shr(a,b)Bitwise Right Shift (>>) (Lua 5.3+)

Example: Vector addition using __add

local Vector = {}
Vector.__index = Vector -- For methods, if any
function Vector:new(x, y)
local vec = {x = x or 0, y = y or 0}
setmetatable(vec, Vector)
return vec
end
Vector.__add = function(vec1, vec2)
return Vector:new(vec1.x + vec2.x, vec1.y + vec2.y)
end
Vector.__tostring = function(vec)
return "Vector(" .. vec.x .. ", " .. vec.y .. ")"
end
local v1 = Vector:new(10, 20)
local v2 = Vector:new(5, 7)
local v3 = v1 + v2 -- Triggers Vector.__add
print(v1) -- Output: Vector(10, 20)
print(v2) -- Output: Vector(5, 7)
print(v3) -- Output: Vector(15, 27)
  • __call(table, arg1, arg2, ...): Allows a table to be called like a function: myTable(arg1, arg2). The first argument to the metamethod is the table itself, followed by the arguments passed in the call.
  • __tostring(table): Called by tostring() (and often by print()) to get a string representation of the table. Must return a string.
  • __gc(table): The finalizer. Called when the table is about to be garbage collected. Useful for releasing external resources. (Available for userdata by default; for tables, only if the table is garbage collectible and has this metamethod set before Lua 5.2 it was only for userdata, now tables can have it if they are collectable via weak tables or no references). Generally, for tables, this is more complex and involves C API or specific conditions for tables that are collectable.

Example with __call and __tostring:

local CallableAdder = {
value_to_add = 0
}
CallableAdder.__call = function(self, num_to_add)
return self.value_to_add + num_to_add
end
CallableAdder.__tostring = function(self)
return "CallableAdder (adds " .. self.value_to_add .. " by default)"
end
local my_adder = { value_to_add = 5 } -- An instance
setmetatable(my_adder, CallableAdder)
print(my_adder) -- Triggers __tostring: Output: CallableAdder (adds 5 by default)
local result = my_adder(10) -- Triggers __call: my_adder acts as self
print("Result of call:", result) -- Output: Result of call: 15 (5 + 10)

Practical Application: Metatables are the foundation of object-oriented programming in Lua. They are also used for creating domain-specific languages (DSLs), proxy tables, read-only tables, and implementing custom data structures with specific behaviors.