setmetatable() (Metatables)
In Lua, metatables allow you to add special behaviors to tables, enabling operator overloading, default value configuration, and object-oriented programming (OOP). Use setmetatable() to attach a metatable to a table and getmetatable() to retrieve it.
Syntax
-- ----------------------------------------------- -- Setting and getting a metatable -- ----------------------------------------------- setmetatable(table, metatable) -- attaches a metatable to table (returns table) getmetatable(table) -- returns the metatable of table (nil if none) -- ----------------------------------------------- -- Key metamethods (fields of the metatable) -- ----------------------------------------------- __index = table or function -- called when a missing key is accessed (basis for inheritance) __newindex = function -- called when a missing key is assigned __tostring = function -- called by tostring() / print() __add = function -- overloads the + operator __sub = function -- overloads the - operator __eq = function -- overloads the == operator __call = function -- called when the table is invoked as a function -- ----------------------------------------------- -- rawget / rawset (bypass the metatable) -- ----------------------------------------------- rawget(table, key) -- retrieves a field without calling __index rawset(table, key, value) -- sets a field without calling __newindex
Syntax Reference
| Function / Field | Description |
|---|---|
setmetatable(t, mt) | Attaches metatable mt to table t. Returns t. |
getmetatable(t) | Returns the metatable set on table t. Returns nil if none is set. |
rawget(t, k) | Gets the value of key k in table t directly, without invoking the __index metamethod. |
rawset(t, k, v) | Sets key k in table t to value v directly, without invoking the __newindex metamethod. |
__index (table) | Specifies a table to look up when a missing key is accessed. Enables prototype-chain inheritance. |
__index (function) | Called as function(t, k) when a missing key is accessed. Used for dynamic default value generation. |
__newindex | Called as function(t, k, v) when a missing key is assigned. Used for implementing read-only tables, among other things. |
__tostring | Called when a table is converted to a string via tostring() or print(). Used for readable object display. |
__add | Called as function(a, b) when a table is added with the + operator. |
__eq | Called when a table is compared with the == operator. Enables custom equality checks even when the operands are not the same reference. |
__call | Called as function(t, ...) when the table is invoked like a function with t(...). |
Sample Code
steinsgate_metatable.lua
-- steinsgate_metatable.lua — basic metatable sample
-- Uses Steins;Gate characters to verify the main metatable features
-- -----------------------------------------------
-- 1. __index (table) — prototype inheritance
-- -----------------------------------------------
-- Define shared information as a prototype table
local LabMemberProto = {
organization = "Future Gadget Laboratory",
greeting = function(self)
return self.name .. " is a lab member."
end,
}
-- Create individual lab member tables and set the prototype as __index
local okabe = setmetatable(
{ name = "Rintaro Okabe", code = "004" },
{ __index = LabMemberProto }
)
local makise = setmetatable(
{ name = "Kurisu Makise", code = "004" },
{ __index = LabMemberProto }
)
print("=== __index (table inheritance) ===")
-- organization does not exist on okabe itself, so it is fetched from the prototype via __index
print(okabe.name .. " belongs to: " .. okabe.organization)
print(makise.name .. " belongs to: " .. makise.organization)
print(okabe:greeting())
print(makise:greeting())
print("")
-- -----------------------------------------------
-- 2. __index (function) — dynamic default values
-- -----------------------------------------------
-- Create a table that returns "unregistered" for any missing key
local member_list = setmetatable({
mayuri = "Mayuri Shiina",
hashida = "Itaru Hashida",
}, {
__index = function(t, key)
return "unregistered: " .. key
end
})
print("=== __index (function) ===")
print("mayuri: " .. member_list.mayuri)
print("suzuha: " .. member_list.suzuha) -- missing key is handled by __index function
print("daru: " .. member_list.daru) -- also unregistered, so the function is called
print("")
-- -----------------------------------------------
-- 3. __newindex — write hook
-- -----------------------------------------------
-- Create a table that logs changes while writing to fields
local log = {}
local watched = setmetatable({}, {
__newindex = function(t, key, value)
-- record the change to the log, then write with rawset
table.insert(log, string.format(" SET %s = %s", key, tostring(value)))
rawset(t, key, value)
end
})
print("=== __newindex ===")
watched.experiment = "Time Machine Experiment"
watched.result = "World Line Divergence: 1.048596"
watched.operator = "Rintaro Okabe"
print("Change log:")
for _, entry in ipairs(log) do
print(entry)
end
print("Saved: " .. watched.experiment)
print("")
-- -----------------------------------------------
-- 4. __tostring — customize print() output
-- -----------------------------------------------
-- Define a metatable for readable character display
local CharMeta = {
__tostring = function(self)
return string.format("[%s] %s (Lab Member No: %s)",
self.code, self.name, self.lab_no)
end
}
local suzuha = setmetatable(
{ name = "Suzuha Amane", code = "007", lab_no = "008" },
CharMeta
)
print("=== __tostring ===")
print(tostring(suzuha)) -- __tostring is called
print(suzuha) -- print() internally uses tostring(), so __tostring is called
print("")
-- -----------------------------------------------
-- 5. __add — operator overloading
-- -----------------------------------------------
-- Define the + operator on a vector table
local VecMeta = {
__add = function(a, b)
return setmetatable({ x = a.x + b.x, y = a.y + b.y }, getmetatable(a))
end,
__tostring = function(v)
return string.format("Vec(%g, %g)", v.x, v.y)
end
}
local pos_okabe = setmetatable({ x = 3, y = 5 }, VecMeta)
local pos_makise = setmetatable({ x = 1, y = 2 }, VecMeta)
local pos_sum = pos_okabe + pos_makise -- __add is called
print("=== __add ===")
print("Okabe's position: " .. tostring(pos_okabe))
print("Makise's position: " .. tostring(pos_makise))
print("Combined position: " .. tostring(pos_sum))
print("")
-- -----------------------------------------------
-- 6. OOP pattern — class implementation
-- -----------------------------------------------
-- Implement a class and instance pattern with metatables
local LabMember = {}
LabMember.__index = LabMember -- set the metatable for instances to the class itself
-- Define the constructor (new method)
function LabMember.new(name, lab_no, iq)
local self = setmetatable({}, LabMember)
self.name = name
self.lab_no = lab_no
self.iq = iq
return self
end
-- Define instance methods
function LabMember:introduce()
return string.format(
"My name is %s. Lab Member #%s, IQ %d.",
self.name, self.lab_no, self.iq
)
end
function LabMember:is_genius()
return self.iq >= 160
end
-- Create instances
local member1 = LabMember.new("Rintaro Okabe", "001", 170)
local member2 = LabMember.new("Kurisu Makise", "004", 204)
local member3 = LabMember.new("Itaru Hashida", "003", 110)
print("=== OOP pattern ===")
print(member1:introduce())
print(member2:introduce())
print(member3:introduce())
print("")
print("Is Okabe a genius? " .. tostring(member1:is_genius()))
print("Is Makise a genius? " .. tostring(member2:is_genius()))
print("")
-- -----------------------------------------------
-- 7. getmetatable / rawget verification
-- -----------------------------------------------
print("=== getmetatable / rawget ===")
print("okabe's metatable: " .. tostring(getmetatable(okabe)))
-- rawget bypasses __index, so prototype fields cannot be retrieved
print("rawget organization: " .. tostring(rawget(okabe, "organization")))
print("normal access organization: " .. okabe.organization)
lua steinsgate_metatable.lua === __index (table inheritance) === Rintaro Okabe belongs to: Future Gadget Laboratory Kurisu Makise belongs to: Future Gadget Laboratory Rintaro Okabe is a lab member. Kurisu Makise is a lab member. === __index (function) === mayuri: Mayuri Shiina suzuha: unregistered: suzuha daru: unregistered: daru === __newindex === Change log: SET experiment = Time Machine Experiment SET result = World Line Divergence: 1.048596 SET operator = Rintaro Okabe Saved: Time Machine Experiment === __tostring === [007] Suzuha Amane (Lab Member No: 008) [007] Suzuha Amane (Lab Member No: 008) === __add === Okabe's position: Vec(3, 5) Makise's position: Vec(1, 2) Combined position: Vec(4, 7) === OOP pattern === My name is Rintaro Okabe. Lab Member #001, IQ 170. My name is Kurisu Makise. Lab Member #004, IQ 204. My name is Itaru Hashida. Lab Member #003, IQ 110. Is Okabe a genius? true Is Makise a genius? true === getmetatable / rawget === okabe's metatable: table: 0x... rawget organization: nil normal access organization: Future Gadget Laboratory
Common Mistakes
setmetatable only accepts tables as its first argument
In standard Lua, only a table can be passed as the first argument to setmetatable(). Strings automatically have a metatable set internally by Lua so that string library methods are accessible. Overwriting this metatable by users is not intended.
-- NG (using setmetatable on a string causes an error)
local s = "hello"
setmetatable(s, { __index = function(t, k) return k end })
-- bad argument #1 to 'setmetatable' (table expected, got string)
-- OK (wrap it in a table)
local s = setmetatable({ value = "hello" }, {
__tostring = function(self) return self.value end
})
print(tostring(s)) -- "hello"
Missing that __metatable field is the cause when getmetatable returns nil
When a __metatable field is set in a metatable, getmetatable() returns that value instead of the metatable itself. This is a mechanism to protect the metatable from external access.
local mt = { __metatable = "protected" }
local t = setmetatable({}, mt)
print(getmetatable(t)) -- "protected" (the value of __metatable is returned, not the metatable)
-- attempting to overwrite the metatable causes an error
local ok, err = pcall(function() setmetatable(t, {}) end)
print(ok, err) -- false, cannot change a protected metatable
Multiple instances sharing a metatable end up sharing fields
Instance fields must always be stored on the instance's own table. Writing directly to the metatable (class table) affects all instances that use that class.
-- NG (storing a field directly on the metatable)
local Animal = {}
Animal.__index = Animal
Animal.name = "unnamed" -- writing to the class shares the value across all instances
local a1 = setmetatable({}, Animal)
local a2 = setmetatable({}, Animal)
a1.name = "Goku" -- written to a1's own table (this part is fine)
print(a2.name) -- "unnamed" (Animal.name is returned via __index)
-- OK (store fields on each instance's own table via a constructor)
function Animal.new(name)
return setmetatable({ name = name }, Animal) -- name is held by the instance itself
end
local a1 = Animal.new("Goku")
local a2 = Animal.new("Vegeta")
print(a1.name, a2.name) -- "Goku" "Vegeta" (independent)
Overview
A metatable is a mechanism for adding special behavior to a Lua table. Attaching a metatable mt to a table t with setmetatable(t, mt) causes metamethods like __index and __newindex to be called automatically during certain operations. Specifying a table as __index enables prototype-chain inheritance, and specifying a function enables dynamic default value generation. __tostring customizes the display when print() is used, and arithmetic metamethods like __add implement operator overloading. In OOP patterns, the common approach is to set ClassName.__index = ClassName and then create instances with setmetatable({}, ClassName). Use rawget() / rawset() when you want to bypass metatable processing. For the basics of table operations, see Table Basics as well.
If you find any errors or copyright issues, please contact us.